How to safely invoke native code in Flutter with Pigeon
Creating Flutter apps that target many platforms is pretty straightforward, but not all code features are available in the Flutter SDK: sometimes you have to write them on your own because each platform has its own way to communicate with its native SDK.
When it comes to writing platform-specific code in Flutter, you need to create a channel to let the data pass through: this platform channel is called MethodChannel. According to the official Flutter docs:
The standard platform channels use a standard message codec that supports efficient binary serialization of simple JSON-like values, such as booleans, numbers, Strings, byte buffers, and Lists and Maps of these (see StandardMessageCodec for details). The serialization and deserialization of these values to and from messages happens automatically when you send and receive values.
The process of writing the interfaces on both Android and iOS hosts plus invoking the channels in the right way is typically error-prone, that’s why the Flutter community has introduced Pigeon, a code generator tool to make communication between Flutter and the host platform type-safe, easier and faster.
For the sole purpose of demonstrating how to invoke native code using Pigeon, I created a Flutter App that shows a list of the latest news by querying the NewsAPI APIs. Let’s deep dive into the process…
Setup the project
The first step requires you to add Pigeon as a dev dependency in your flutter project. In order to do this simply type in your terminal the following command:
flutter pub add --dev
Defining the interface in Flutter
Once we’ve added the pigeon dependency, we need to create the interface that we will use to invoke the native code. Since this is a service in a clean code architecture approach, I created a file named pigeon.dart under the lib/services folder.
Inside this file we need to create an abstract class annotated with the @HostApi annotation referenced in the package:pigeon/pigeon.dart file. It’s important that you also define your DTOs inside this file too.
Here’s my pigeon.dart file, as you can see we have the ArticleApi host API with a single method that returns a list of articles from the native and the Article DTO that carries all the information of single news.
Generating the code
The next step consists of letting Pigeon do its job by generating the code starting from our pigeon.dart file. Open your terminal and type
flutter pub run pigeon \
--input lib/services/pigeon.dart \
--dart_out lib/services/pigeon.g.dart \
--objc_header_out ios/Runner/pigeon.h \
--objc_source_out ios/Runner/pigeon.m \
--java_out ./android/app/src/main/java/it/angelocassano/news_app/Pigeon.java \
with this command, we are telling pigeon to generate some dart code in the pigeon.g.dart file next to the pigeon.dart file, and to generate the platform-specific code for both Android and iOS. Just make sure the folders where the files will be generated exist.
Final steps on Flutter
The NewsRepository class will take care of invoking the articles() method from the generated pigeon.g.dart file as follows.
Switching to Android
To query the NewsAPI APIs, I’ve plugged Retrofit as an Android dependency in my build.gradle file. I also created a new DTO class called EverythingResponse and a service called PigeonArticleApi. This last one will take care of gathering data from the APIs using Retrofit and send back the response to Flutter with the Pigeon autogenerated wrapper.
To let the PigeonArticleApi class be invokable by Flutter, we need to register it after spawning the Flutter Engine. Inside our MainActivity class let’s override the configureFlutterEngine method and invoke the Pigeon.ArticleApi.setup method passing the Flutter Engine’s binary messenger and a new PigeonArticleApi instance as follows.
In my case, I also had to create a Retrofit instance required as a dependency by the PigeonArticleApi class to query the NewsAPI APIs.
Switching to iOS
Before writing our code, remember to add both pigeon.h and pigeon.m inside our XCode project under the Runner group.
Since the generated code is not swift code, we also need to import the pigeon.h header file inside the Runner-Bridging-Header.h file in order to let the autogenerated objective-C code be visible by the swift classes that we will create in the next steps.
To query the NewsAPI APIs, I’ve plugged Alamofire as an iOS dependency in my Podfile file. I also created a new DTO class called EverythingResponseDTO and a service called PigeonArticleApi. This last one will take care of gathering data from the APIs using Alamofire and send back the response to Flutter with the Pigeon autogenerated wrapper.
To let the PigeonArticleApi class be invokable by Flutter, we need to register it after spawning the Flutter Engine. Inside our AppDelegate class let’s invoke the ArticleApiSetup method passing the Flutter Engine’s binary messenger and a new PigeonArticleApi instance under the didFinishLaunchingWithOptions method as follows.
Once we’ve done all of this stuff, we are ready to launch our application on both Android and iOS devices to see if everything works.
Thank you for reading 👋
I hope you enjoyed this article. If you have any queries or suggestions please let me know in the comments down below.