This post is for Day 17 of Mercari Advent Calendar 2024, brought to you by @howie.zuo from the Mercari Hallo mobile team.
Introduction
Hello! I’m @howie.zuo, an engineer on the Mercari Hallo mobile team. In this article, I will guide you through the process of generating type-safe native bridges using Pigeon.
Flutter is an incredibly powerful framework. With a vast ecosystem of community-supported plugins, you usually only need to write a minimal amount of native code to create a mobile application. However, finding the right plugin that meets your product’s needs can sometimes be challenging. Even worse, the perfect plugin may have already been deprecated. Therefore, it’s essential to think carefully before adopting a plugin, especially if maintainability and security are critical for your project.
While working on a feature to interact with the calendar app in Mercari Hallo, I discovered that the only suitable plugin I found wasn’t being actively maintained and had poor code quality, as evident from its GitHub repository. As a result, I decided to build the functionality myself.
Note: The code examples in this article are simplified for demonstration purposes. You may need to adjust them for your own codebase. The implementation specifics regarding calendar interactions are not included here, as we’ll focus primarily on Pigeon.
What is Pigeon?
Borrowed the description from here since it describes clearly enough.
Pigeon is a code generator tool used to make communication between Flutter and the host platform type-safe, easier, and faster.
Pigeon removes the necessity to manage strings across multiple platforms and languages. It also improves efficiency over common method channel patterns. Most importantly though, it removes the need to write custom platform channel code, since pigeon generates it for you.
Installation
-
Start by installing the latest version of Pigeon (22.7.0 as of this writing) in your project’s
pubspec.yaml
:dev_dependencies: pigeon: ^22.7.0
-
Optionally, run
dart pub get
if your environment doesn’t automatically refresh dependencies.
Configuration
-
Create a folder named
pigeon
at the root of your project, and then create a file namedmessage.dart
inside thepigeon
directory.ROOT_PATH_OF_YOUR_PROJECT/pigeon/message.dart
You can choose a different file structure or naming convention if it suits you better.
-
Import the Pigeon package at the top of your
message.dart
file:import 'package:pigeon/pigeon.dart';
-
Define the input data structures:
class Request { Request({ required this.payload, required this.timestamp, }); Payload payload; int timestamp; } class Payload { Payload({ this.data, this.priority = Priority.normal, }); String? data; Priority priority; } enum Priority { high, normal, }
You can find a list of supported data types here. Pigeon also supports custom classes, nested data types, and enums. In Swift, Kotlin, and Dart, you can use
sealed
classes for a more organized data structure. -
Configuration settings
Place the following code at the top of your
message.dart
file. This tells Pigeon how you want it to generate the code:@ConfigurePigeon( PigeonOptions( dartOptions: DartOptions(), dartOut: 'lib/pigeon/message.g.dart', kotlinOptions: KotlinOptions( package: 'com.example.pigeon', ), kotlinOut: 'android/app/src/main/kotlin/com/example/pigeon/Message.g.kt', swiftOptions: SwiftOptions(), swiftOut: 'ios/Runner/Pigion/Message.g.swift', ), )
Pigeon options also support other languages like C, Java, and Objective-C.
-
Define the output data structures and method interface.
Add the following code at the end of your
message.dart
file:class Response { Response({ this.result, }); String? result; } @HostApi() abstract class MessageApi { bool isAvailable(); @async Response send(Request req); }
The
@HostApi()
annotation is used for procedures defined on the host platform that can be called by Flutter. Conversely,@FlutterApi()
is for procedures defined in Dart that you want to call from the host platform.The
@async
annotation indicates that the method is asynchronous.
Code generation
Once the interface is defined, generate the code by running:
flutter pub run pigeon --input pigeon/message.dart
This command will generate code for each platform:
lib/pigeon/message.g.dart
android/app/src/main/kotlin/com/example/pigeon/Message.g.kt
ios/Runner/Pigeon/Message.g.swift
Android Implementation
-
Create a class named
MessageHandler
that implements theMessageApi
interface:class MessageHandler : MessageApi { fun setUp(message: BinaryMessenger) { MessageApi.setUp(message, this) } override fun isAvailable(): Boolean { // your logics go here return true } override fun send(res: Request, callback: (Result<Response>) -> Unit) { // get the input val data = res.payload.data // your logics go here // return result asynchronously use callback callback(Result.success(Response())) } }
isAvailable
andsend
are the methods we defined earlier. Feel free to implement your own logic inside these methods to handle requests from the Flutter side. -
You may have noticed the
setUp
method; we’ll use this to attach theMessageHandler
to the Flutter engine. OverrideconfigureFlutterEngine
inMainActivity
(if it isn’t already present):class MainActivity: FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) // setup the event handler MessageHandler().setUp(flutterEngine.dartExecutor.binaryMessenger) } }
That’s the Android part done. Now let’s move on to the iOS implementation.
iOS Implementation
-
Similarly to Android, create a class named
MessageHandler
that implements theMessageApi
protocol:class MessageHandler : MessageApi { func setUp(binaryMessenger: FlutterBinaryMessenger) { MessagesApiSetup.setUp(binaryMessenger: binaryMessenger, api: self) } func isAvailable() throws -> Bool { // your logics go here return true } func send(res: Request, completion: @escaping (Result<Response, any Error>) -> Void) { // get the input let data = res.payload.data // your logics go here // return result asynchronously use callback completion(.success(Response())) } }
The class structure is quite similar to the one we created for Android.
-
Just like in Android, we need to attach
MessageHandler
to the Flutter engine here as well. OpenAppDelegate.swift
and insert the following lines insideapplication(_:didFinishLaunchingWithOptions:)
:let controller : FlutterViewController = window?.rootViewController as! FlutterViewController MessageHandler().setUp(binaryMessenger: controller.binaryMessenger)
Flutter
Finally, let’s see how to call the host platform methods from Flutter.
-
For
isAvailable
final messageApi = MessageApi(); final isAvailable = messageApi.isAvailable();
-
For
send
, which is an asynchronous function:final messageApi = MessageApi(); final res = await messageApi.send(Request( payload: Payload( data: 'Hello, Pigeon!', priority: Priority.normal, ), timestamp: DateTime.now().millisecondsSinceEpoch, ));
The code above is straightforward and should be easy to understand.
A few more things
- Pigeon also supports macOS, Windows, and Linux.
- There are more features not covered in this article that you can explore, such as
EventChannelApi
. - As a Flutter engineer, you don’t need to be an expert in platform-specific languages, but having some experience in Android or iOS development will undoubtedly be helpful in the development of native API-dependent functionality.
Reference
Some of the resources you may also find useful
https://docs.flutter.dev/platform-integration/platform-channels
https://pub.dev/packages/pigeon
https://github.com/flutter/packages/blob/main/packages/pigeon/example/README.md
Tomorrow’s article will be by @naka. Look forward to it!