DEV Community

loading...
Cover image for Mastering Flutter: Create a plugin

Mastering Flutter: Create a plugin

theotherdevs profile image TheOtherDev/s ・7 min read

This story starts when one of your clients or a designer asks you to implement a feature that can be done using native APIs but it has not included on Flutter yet.

The first thing that you try to do a little search on pub.dev, someone has been in my situation and has developed a package to resolve this situation and has been kind enough to share it with the community. But unfortunately you don't find anything that solves your problem, you have only one solution left: TO CREATE A PLUGIN.

But wait, first we should tell you what is a plugin. There are two types of packages in the Flutter ecosystem:

  • Dart packages: general packages with only dart code (e.g. the path package)
  • Plugin packages: special packages that combine native code with a dart interface and allow you to use platform-specific code (e.g. the url_launcher package)

So...let's do a plugin

If you're using Android Studio you can go under File -> New -> New Flutter Project... and then select Flutter Plugin

Android Studio menu

Flutter Plugin

You can also create a plugin using the command line:

flutter create --template=plugin
Enter fullscreen mode Exit fullscreen mode

You can also specify the platform for which you want to develop the plugin and the language you will use

flutter create --org com.example --template=plugin --platforms=android,ios -I swift -a kotlin flutter_is_awesome
Enter fullscreen mode Exit fullscreen mode

This will create a plugin called flutter_is_awesome with the iOS part in Swift and the Android part in Kotlin.

We will create a plugin that will show the system contacts picker,

Plugin Anatomy

This is what will be created:

Structure

We have:

  • The android folder: we will put all the Android specific code here.
  • The iOS folder: the same as above, but for iOS.
  • The lib folder: this folder will contain all the Dart part of the plugin, that will be invoked by the application.
  • The test folder: Here you will develop the tests for your plugin.
  • The example folder: An application that imports our package as dependency, you can use this to try out the plugin without importing it in another app.

The plugin uses a a channel called MethodChannel to communicate between the Dart part and the native part, it's like a tunnel where the dart part sends messages and the native part listen to those messages and react to them. It can be used also to send messages from the native part to the dart part, but this will not be covered in this article.

Our plugin will have just one function that will show the native contacts picker, let the user choose one contact, and return the name of the contact selected to the user.

In this example we will return a simple string to the Dart part, but you can return more complex data structures using for example a map, or a list, you can find the list of the data type supported by the plugins here.

Now that we've created the plugin and defined what it will do we need to do those those things:

  • Write the Android part of the plugin
  • Write the iOS part of the plugin
  • Write the dart part of the plugin

Android Code

When the plugin has been created the default class has been generated, it contains two methods: onAttachedToEngine and onMethodCall. The first one is called when the plugin is initialized, it creates the channel of communication with the dart part and begins to listen to the channel for messages. The second is invoked whenever there's a new message in the channel; it has 2 parameters: call which contains the details of the invocation (the method and eventual parameters) and result that will be used to send the result back to the dart part.

class FlutterIsAwesomePlugin: FlutterPlugin, MethodCallHandler {

  override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
    channel = MethodChannel(flutterPluginBinding.binaryMessenger, "flutter_is_awesome")
    channel.setMethodCallHandler(this)
  }

  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    if (call.method == "getAContact") {

    } else {
      result.notImplemented()
    }
  }
Enter fullscreen mode Exit fullscreen mode

Let's define some variable for our plugin that will be use later

  val PICK_CONTACT_RESULT_CODE = 36
  var act: android.app.Activity? = null
  private lateinit var channel : MethodChannel
  private lateinit var result: Result
Enter fullscreen mode Exit fullscreen mode

Now we need to implement the ActivityAware and PluginRegistry.ActivityResultListenerprotocols, we need those to retrieve the activity that will be used to show our contact picker.

  override fun onAttachedToActivity(binding: ActivityPluginBinding) {
    act = binding.activity
    binding.addActivityResultListener(this)
  }

  override fun onDetachedFromActivityForConfigChanges() {
    act = null;
  }

  override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
    act = binding.activity
    binding.addActivityResultListener(this)
  }

  override fun onDetachedFromActivity() {
    act = null;
  }

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
    return false
  }
Enter fullscreen mode Exit fullscreen mode

The only part that's missing is the code to show the contacts picker and retrieve the result. In our onMethodCall start the activity to choose a contact and in onActivityResultretrieve the result and send it to the dart part using the result object that we've previously saved.

  override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
    this.result = result //saves the result to call it when the user selects a contact
    if (call.method == "getAContact") {
      val intent = Intent(Intent.ACTION_PICK, ContactsContract.Contacts.CONTENT_URI)
      act?.startActivityForResult(intent, PICK_CONTACT_RESULT_CODE)
    } else {
      result.notImplemented()
    }
  }

...

  override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?): Boolean {
    if (requestCode == PICK_CONTACT_RESULT_CODE) {
      if (resultCode == Activity.RESULT_OK) {
        if (data != null) {
          val contactData = data.data
          val c = act!!.contentResolver.query(contactData!!, null, null, null, null)
          if (c!!.moveToFirst()) {
            val name = c.getString(c.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME))
            result.success(name)
            return true
          }
        }
      }
    }
    return false
  }
Enter fullscreen mode Exit fullscreen mode

The Android part is done! βœ…

iOS Code

The iOS part will be very similar to the Android one, here we have the register function, which is the equivalent of the onAttachedToEngine that we've seen for Android, and the handle function, that will be called when new messages will arrive.

public class SwiftFlutterIsAwesomePlugin: NSObject, FlutterPlugin {
  public static func register(with registrar: FlutterPluginRegistrar) {
    let channel = FlutterMethodChannel(name: "flutter_is_awesome", binaryMessenger: registrar.messenger())
    let instance = SwiftFlutterIsAwesomePlugin()
    registrar.addMethodCallDelegate(instance, channel: channel)
  }

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    if call.method == "getAContact" {

    } else {
        result(FlutterMethodNotImplemented)
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Now we need to create a new class ContactPickerDelegate that will receive the callback of the contacts picker view controller (when a contact is selected or when the picker is dismissed) and will inform the plugin class. It has 2 blocks variable onSelectContact and onCancelthat will be invoked when the picker will inform this class with the CNContactPickerDelegate protocol.

import Foundation
import ContactsUI

class ContactPickerDelegate: NSObject, CNContactPickerDelegate {
    public var onSelectContact: (CNContact) -> Void
    public var onCancel: () -> Void

    init(onSelectContact: @escaping (CNContact) -> Void,
         onCancel: @escaping () -> Void) {
        self.onSelectContact = onSelectContact
        self.onCancel = onCancel
        super.init()
    }

    func contactPicker(_ picker: CNContactPickerViewController, didSelect contact: CNContact) {
        picker.presentingViewController?.dismiss(animated: true, completion: nil)

        onSelectContact(contact)
    }

    func contactPickerDidCancel(_ picker: CNContactPickerViewController) {
        picker.presentingViewController?.dismiss(animated: true, completion: nil)
        onCancel()
    }
}
Enter fullscreen mode Exit fullscreen mode

What we need to do now is to display the contacts picker when the getAContact message is received, so let's add this to the plugin class:

  public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
    if call.method == "getAContact" {
        getAContact(withResult: result)
    } else {
        result(FlutterMethodNotImplemented)
    }
  }

   //save the contact picker, so it's not deallocated    
    var contactPickerDelegate: ContactPickerDelegate?
    private func getAContact(withResult result: @escaping FlutterResult) {
        let contactPicker = CNContactPickerViewController()
        contactPickerDelegate = ContactPickerDelegate(onSelectContact: { contact in
            //sends the result back to dart
            result(contact.givenName + contact.familyName)
            self.contactPickerDelegate = nil //set to nil, so it's removed from memory
        },
        onCancel: {
            result(nil)
            self.contactPickerDelegate = nil //set to nil, so it's removed from memory
        })
        contactPicker.delegate = contactPickerDelegate
        let keyWindow = UIApplication.shared.windows.first(where: { $0.isKeyWindow })
        let rootViewController = keyWindow?.rootViewController
        DispatchQueue.main.async {
            rootViewController?.present(contactPicker, animated: true)
        }
    }
Enter fullscreen mode Exit fullscreen mode

The iOS part is done! βœ…

Dart Code

The last piece of the puzzle 🧩 is to create the Dart part of the plugin that will glue all the other pieces toghether.

class FlutterIsAwesome {
  static const MethodChannel _channel =
      const MethodChannel('flutter_is_awesome');

  static Future<String> getAContact() async {
    final String contact = await _channel.invokeMethod('getAContact');
    return contact;
  }
}
Enter fullscreen mode Exit fullscreen mode

As you can see it's very simple, it invokes the method getAContact on the channel and waits for the result and returns it. Note that all the functions that interact with the native part are asynchronous and will return a Future.

Now we only need to test out our plugin, we've done a simple app in the example folder, with just one button and a label to test out if everything is working fine.

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_is_awesome/flutter_is_awesome.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  String _contact = 'Unknown';

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('Flutter is Awesome'),
        ),
        body: Center(
          child: Column(
            mainAxisAlignment: MainAxisAlignment.center,
            children: [
              MaterialButton(
                color: Colors.red,
                textColor: Colors.white,
                child: Text('Picker'),
                onPressed: () => _getAContact(),
              ),
              Text(_contact ?? '')
            ],
          ),
        ),
      ),
    );
  }

  _getAContact() async {
    String contact;
    try {
      contact = await FlutterIsAwesome.getAContact();
    } on PlatformException {
      contact = 'Failed to get contact.';
    }
    if (!mounted) return;
    setState(() {
      _contact = contact;
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

And here's the final result:

Android

iOS

Conclusions

You can find the final code of the plugin at this GitHub link.

What we've developed here is a basic example that returns just a string to the dart part but, as said before you could return a more complex data structure and you can also pass parameters to the native part when calling the invokeMethod functions.
The things that you can do with the plugins are almost infinite, we've just scratched the surface, the limit is your imagination.

Discussion (0)

pic
Editor guide