Flutter Plugins and Protocol Buffers a definitive guide.

martin seal
11 min readDec 6, 2020

--

This is my first article and it aims to show you a full example from start to finish on how to create a flutter plugin and use it from another app, passing data back and forth using protocol buffers, this will only show examples for Android written in Kotlin (rip iOS)

This is only my second attempt at this so if you see flaws in what I’m doing please let me and everyone else know and I’ll update this, there are other guides out there that were a massive help to me and will be linked below, but I felt they weren’t as complete as this one aims to be. By the end of this tutorial you will have a working flutter plugin that sends messages from flutter to android and vice versa, let’s get into it.

Prerequisites: a computer, a flutter environment (flutter SDK set up) an android environment (android studio set up), intermediate knowledge of Flutter Dart Android and Kotlin.

TLDR: Use protocol buffers to serialise data to byte arrays and deserialise them in Android or iOS

An example can be found here https://github.com/martipello/flutter_plugin_protobuf_example

I’m using Android Studio 4.0.1 Build #AI-193.6911.18.40.6626763

The first thing we’re going to do is create a new flutter package which is documented here https://flutter.dev/docs/development/packages-and-plugins/developing-packages and is straightforward enough so lets go through the steps. First we want a package that calls Android or iOS code and can send results to the DART SIDE, ie: Flutter, and since we will be the author of the package we can skip the stuff about federated plugins, we will create a package using the defaults which will use Swift in iOS and Kotlin on android so open up a terminal, navigate to the folder you want and run the command:

flutter create --org com.example --template=plugin --platforms=android,ios hello

swapping out the com.example after the — org for your package name and the ‘hello’ at the end for the name you want to give to your plugin:

for example mine will be :

flutter create --org com.sealstudios --template=plugin --platforms=android,ios my_plugin

From the docs:

This creates a plugin project in the hello folder with the following specialized content:lib/hello.dartThe Dart API for the plugin.android/src/main/java/com/example/hello/HelloPlugin.ktThe Android platform-specific implementation of the plugin API in Kotlin.ios/Classes/HelloPlugin.mThe iOS-platform specific implementation of the plugin API in Objective-C.example/A Flutter app that depends on the plugin, and illustrates how to use it.When this completes you’ll get an “All done!” message and flutter will run flutter doctor

So we now have a Flutter plugin with an Android folder for our Android code, an iOS folder for our iOS code and an example Flutter app which can use both, (something that kept confusing me is this flutter example app also has an Android and iOS folder inside it, we will ignore these entirely) now lets open our plugin, to do this open Android studio, and either use file>open or from the start menu select ‘open an existing project’, now select out plugins root folder and click open:

Congrats we have created a plugin, next step method channels!

To call code inside of Android or iOS from Flutter or the other way round (calling code in Flutter from Android or iOS) we use method channels.

They pass messages and data back and forth from Flutter to native platforms with the help of result messages.

Now on the flutter docs https://flutter.dev/docs/development/platform-integration/platform-channels?tab=android-channel-kotlin-tab#pigeon they recommend Pigeon as a type safe way of calling method channels and passing data, I’ve looked into this and it looks very promising but some things are just broken right now and it is still in early access preview so we can’t currently use that but if you’re reading this and it’s not, go and use that as it is safer.

First let’s look at our example/lib/main.dart it should look like this:

I didn’t like this so decided to change it to look like this:

It will do the same thing but has much less to look at, but the main thing here is the platform version call MyPlugin.platformVersion this is a static method in your plugin.

This method calls channel.invokeMethod() this method takes a string which we will use as an ID for which method to call on the other side, the method also takes an ‘arguments’ property which we will use to pass data back and forth, more on this later.

Channels can be accessed from Flutter and from Kotlin so we can call channel.invokeMethod() from Android to send things to Flutter and we can call channel.invokeMethod() from Flutter to send things to Android and the same can be said for iOS, you can see concrete examples of this in the example linked at the start of the article.

To receive those methods, or react to them, we use a method handler you can see we already have one in your android folder android/src/main/kotlin/com.sealstudios.my_plugin/MyPlugin.kt

I wanted to make this look a little more Kotliny so I changed it to read

But they should do the same thing, so it checks for the string “getPlatformVersion” and executes whatever you want to do, in this case we just call a method called getPlatformVersion() and pass it the result, we don’t need to return anything here but we do need to do something with the result property, we can call result.success, result.error, or result.notImplemented() this will return something to the caller. result.success takes whatever object we like, result.error takes an error code, an error message and an errorDetail object, result.unimplemented takes nothing, in this case we give result.success a string “Android ${android.os.Build.VERSION.RELEASE}” and this is given back to the example app that called the invokeMethod. As mentioned this can be done from Flutter to android or vice versa again you can see concrete examples of this in the example.

🎉 Congrats 🎉 We’ve finished with method channels, next step protocol buffers! 💪

Method channels also allow us to pass arguments,

The problem with these are they are dynamic so they aren’t type safe, meaning we could make silly mistakes that are hard to find and fix, protocol buffers are not a silver bullet but they really help so lets get to, first let’s install it, on mac open a terminal and run

brew install protobuf

For the swift plugin run

brew install swift-protobuf 

now we need the dart plugin so run

pub global activate protoc_plugin 

next for android we can grab the plugin for android studio so in android studio open the android studio menu and select preferences/plugins and search for protocol buffers and install the plugin

Once we have that we need to add protocol buffers to the project so first let’s add the dart package, open your plugins root pubspec.yaml and add the protobuf package, I’ll also be adding universal io

protobuf: ^1.1.0universal_io: ^1.0.1

Now open your plugins android folders build.gradle, when you do android studio will give you the option to open it in android studio for editing, select this as it will help with intellisense

First add this to your dependencies

ext.protobuf_version = '0.8.10'classpath "com.google.protobuf:protobuf-gradle-plugin:$protobuf_version"

Next add the new plugin

apply plugin: 'com.google.protobuf'

Now add this

Im going to add java 8 support

Increase my min sdk (this isnt required)

defaultConfig {     minSdkVersion 21}

Add source sets for protobuf

main.proto.srcDirs += '../protos'

And then add dependencies for protobuf and gson

So your full file should look like this:

🤯 Wow that was a lot of work don’t forget to select sync project with gradle files and were done you can now close this window to go back to the one we were in (my_plugin)

So now we have it set up what are we going to do with it? 🤔 We we are going to create objects which give us a few convenience methods for converting to maps, json, bytes, byte arrays and I think theres more but we only need the byte array stuff, this will allow us later to create an object, convert it, pass it to whatever platform through the method channel and then recreate our model from it on the other side, this tutorial will use byte arrays in Kotlin using the methods .toByteArray() and .parseFrom(), and we will use Uint8List in dart using the methods .fromBuffer() and .writeToBuffer()

Let’s start. 😊

Create a new folder called protos in your plugin root folder

Now create a new file called models.proto

Now lets create a message, I’m new to protos, the language tour can be found here https://developers.google.com/protocol-buffers/docs/overview but I wanted to create a real world example so we’re gonna create a message that holds a date, a string, and an enum and I’ll also show you how to send lists, so first our models.proto looks like this:

Here i’ve used a Timestamp this is achieved by using googles timestamp.proto file and including it in the same protos folder this can be seen and downloaded from the example now we need to generate the models, I ran into issues here hopefully you don’t there is more information and an example in the root README.md.

First to create the Dart models in your root/lib folder create a folder called generated and then run this command in a terminal from the root directory of our project:

protoc --plugin=protoc-gen-dart=$HOME/.pub-cache/bin/protoc-gen-dart --dart_out=lib/generated -Iprotos protos/*.proto

If it went well you should have something that looks like this:

If it didn’t give you an error but you don’t see the generated models try opening and closing the lib folder.

If you did get an error it could be to do with several things and honestly Google is your friend here an error I had to overcome was this

After this is complete we need to generate the Android Java models this step for me was super flaky so the first thing I did was open the android folder in Android studio, theres multiple ways to do this the quickest IMO is open the file root/android/src/main/kotlin/com.sealstudios.my_plugin/MyPlugin.kt and this should give you a prompt to open it in android studio (open for editing in android studio), when that opens, open the terminal and run the command:

./gradlew clean

this will delete the java generated files, now build the project using the play button this will regenerate those files and you will have your newly created models at

root/example/build/my_plugin/generated/source/proto/debug/javalite/com/sealstudios/my_plugin

WOW!!! That was a lot of work! If you’re still here congrats 🥳 ! This is where all that hard work starts to pay off 👍

Lets start using these models

The first thing I’m going to do is make our plugin a singleton and remove the plugin example getPlatformVersion method so lets change the my_plugin.dart file from this:

To this:

Now lets create an abstract class with all our methods, these are the methods that the android code will call and we will receive these calls in the flutter app

Add a property to hold this class and add the method channel methods to our plugin it should now look like this:

Now we need to make some methods for when these things are called but before we do lets make a small extension on MethodCall that looks like this:

Now our methods can look like this:

Don’t forget the extension import, mine is

import './extensions/extensions.dart';

We also do the same thing for the list check out the example for the code.

Now we have this lets open our root/example/lib/main.dart, we have an error because we removed the call for MyPlugin.platformVersion, we can ignore this for now or just comment it out if its looking at you funny 😂 , now where we extend the MyApp state we are going to implement our abstract class and create our 2 missing overrides,

once we have this we also need to initialise it so in our init state initialise the delegate:

And now we have communication from Android to Flutter 😎 lets create the communication the other way, back in your my_plugin.dart we can create some methods to go and talk to android, these can send data or retrieve data, lets create one that just sends something to android:

This will send that example data over to android and is expecting a return of future bool this will be the method channels success message, lets create that now.

Jump into your root/android/src/main/kotlin/com.sealstudios.my_plugin/MyPlugin.kt file, first thing we want to do is write another small extension method for getting those tasty tasty bytes 😋

Now delete the getPlatformVersion method since its no longer needed and lets create our method for sendExampleDataToAndroid():

Remember this doesn’t need to return anything but we do pass true to result.success which will tell flutter this was successful and return true to the calling method, now update your onMethodCall:

And we can now call this from our flutter code using the method we created, so in main.dart you can now call something like:

But what if we want to return something 🤔? Well then we put it in that result.success method. Lets create another method that takes a Failed dummy data and converts it to success before passing it back. Open your MyPlugin.kt file again and add a new method:

This creates a new object and hands it back through the result.success method don’t forget to add it to our method handler:

And back in Flutter we can call it with a future builder as the result method will always be asynchronous. Lets replace our old getPlatformVersion method with this, so in my_plugin.dart create the method:

And back in main.dart let’s use it:

And that as they say is that, with lists its exactly the same process the only caveat is that they should be there own explicit object but its included in the example link at the top of the page if your interested.

Thanks for reading, and let me make it abundantly clear that this should serve as a base please send pull requests and I’ll update the example and this article, and build on it, also I never would have managed this if it wasn’t for some great articles out there including but not limited too :

also a special thanks to Lukasz Ciastko, once you get a handle I’ll add ya 😉

--

--