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
You can also create a plugin using the command line:
flutter create --template=plugin
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
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:
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()
}
}
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
Now we need to implement the ActivityAware
and PluginRegistry.ActivityResultListener
protocols, 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
}
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 onActivityResult
retrieve 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
}
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)
}
}
}
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 onCancel
that 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()
}
}
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)
}
}
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;
}
}
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;
});
}
}
And here's the final result:
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.
Top comments (1)
Helpful article! Thanks! If you are interested in this, you can also look at my article about Flutter templates. I made it easier for you and compared the free and paid Flutter templates. I'm sure you'll find something useful there, too. - dev.to/pablonax/free-vs-paid-flutt...