by TruongSinh Tran-Nguyen

How to set up Flutter platform channels with Protobuf

This post is intermediate-advanced level. It is aimed for the audience who is going to write custom platform-specific code as a Flutter plugin.

TLDR: When writing platform-specific code as Flutter plugins, you should use ProtoBuf for type-safety, high performance, and a superb development experience. The example code and all 5 steps are available on my GitHub.

The Problem

When writing platform-specific code as Flutter plugins, one of the key things is data transfer between the Dart side and the platform side. Under the hood, it’s just a bunch of binary 0s and 1s. Though all the code we’re dealing with (Dart, Java/Kotlin, ObjC/Swift) is typed, so Flutter makes it easier to have some typing:

However, looking at the table above, you will notice several things:

  • FlutterStandardTypedData family is troublesome. (Believe me, been there done that 😫)
  • Any more complicated structure than those more or less “primitive” types will need to use List<dynamic>; and Map<String, dynamic>;, in which dynamic should be a red flag.

Let’s look at my common error casting List (and the same happens with Map):

and how verbose it is to do correctly:

This is just a top level List/Map, imagine you have to go deep into the data structure that you have to pass back and forth between Dart and platform-specific:

So, to sum up:

  • FlutterStandardTypedData is frustrating.
  • Casting data is a nightmare.
  • When dealing with List/Map , we lose type-safety (especially with typos in Map’s key, or refactoring code/structure).
  • List<dynamic>; and Map<String, dynamic> are not particularly good in terms of performance.

The solution

Protocol Buffers, aka Protobuf, is a language-neutral, platform-neutral, extensible mechanism for serializing structured data, which happens to support:

So, let’s deep dive!

Prepare the project

I will create the plugin project with Kotlin and Swift (because I love them), it is the same for Java and ObjC anyway.

flutter create -t plugin -i swift -a kotlin plugin_with_protobuf

Then you should see

All done!
[✓] Flutter is fully installed. (Channel master, v1.4.2, on Mac OS X 10.14.3 18D109, locale en-US)
[✓] Android toolchain - develop for Android devices is fully installed. (Android SDK version 28.0.3)
[✓] iOS toolchain - develop for iOS devices is fully installed. (Xcode 10.2)
[✓] Android Studio is fully installed. (version 3.3)
[✓] VS Code is fully installed. (version 1.32.3)
[!] Connected device is not available.
Run "flutter doctor" for information about installing additional components.
In order to run your application, type:
$ cd plugin_with_protobuf/example
$ flutter run
Your application code is in plugin_with_protobuf/example/lib/main.dart.
Your plugin code is in plugin_with_protobuf/lib/plugin_with_protobuf.dart.
Host platform code is in the "android" and "ios" directories under plugin_with_protobuf.
To edit platform code in an IDE see https://flutter.io/developing-packages/#edit-plugin-package.

Now run the project to make sure everything’s right. I assume you already have a device connected or simulator/emulator running

cd plugin_with_protobuf/exampleflutter run

Or easier, use your preferred IDE, either VS Code or Android Studio / IntelliJ. Anyway, you should have this:

The current code at this step is available on GitHub.

Prepare environments

  • Install Protobuf compiler: brew install protobuf on Mac, or see the detailed instruction in README.
  • Install Swift-plugin for Protobuf compiler: brew install swift-protobuf on Mac, or see the detailed instruction in README.
  • Install Dart-plugin for Protobuf compiler: pub global activate protoc_plugin
  • Install Protobuf extension for IDEs

Create proto

Now let’s use an IDE. I’m using both VS Code or Android Studio, but for this one, I will use Android Studio. Open the project plugin_with_protobuf (not plugin_with_protobuf/example) with Android Studio. Then create a new directory called protos , and create a new file person.proto

The current code at this step is available on GitHub.

Generate proto in Dart

You can see from the first 2 lines in person.proto, run the first commands to generate Dart code (you might want to create gen directories beforehand).

protoc --dart_out=./lib/gen ./protos/person.proto

In pubspec.yaml , add a dependency for Protobuf runtime as well:

The current code at this step is available on GitHub.

Generate proto in Swift

Similar to step 2:

protoc --swift_out=./ios/Classes ./protos/person.proto

In ios/plugin_with_protobuf.podspec , add a dependency for Protobuf runtime as well, note that SwiftProtobuf 1.4 requires minimum iOS 9.0.

The current code at this step is available on GitHub.

Send data from Swift and receive in Dart

Open XCode from Android Studio:

Create mock data (mock data creation is abbreviated, you can see full diffs on GitHub), serialize and send it to Dart.

Back to Android Studio, receive and deserialize data:

Some other UI changes are abbreviated, you can see full diffs on GitHub. Now it seems to be working.

The current code at this step is available on GitHub.

Generate proto and send data from Kotlin

Unlike steps 2 and 3, protos in Java/Kotlin can be automatically generated from Gradle. We just need to use protobuf-gradle-plugin.

Similar to step 4, create mock data (mock data creation is abbreviated, you can see full diffs on GitHub), serialize and send it to Dart.

And because we can already receive data from Dart and display, it “just works.”

The current code at this step is available on GitHub.

Conclusion

Communication between Dart and platform-specific code, especially when it involves complicated data structures, should use a type-safe and high-performance serialization tool, such as ProtoBuf (for example, BuiltValue is more or less type-safe but not as high-performant). It is fortunate that ProtoBuf supports all 5 languages and build tools require for Flutter, and is easy to integrate.

Final note: what kind of unit test / integration test do you think we should have for this example? 😏