Flutter native communication with Pigeon
October 9, 2024
Flutter is an incredibly powerful and versatile framework for building cross-platform applications. One of its standout features is the ability to communicate with platform-specific (native) code using platform channels. This allows developers to tap into features provided by the native platform that may not have been directly exposed in Flutter.
When dealing with native communication, maintaining a clean and structured codebase can sometimes be a challenge, especially if the communication becomes complex. This is where Pigeon comes in. Pigeon is a code generator that simplifies and automates Flutter's platform channel setup by generating type-safe code for both the Dart and native sides of communication.
Benefits of using Pigeon
- Type safety: Both Dart and native code are strongly typed, reducing the likelihood of runtime errors.
- Boilerplate reduction: Automatically generates the required Dart, Java/Kotlin, and Objective-C/Swift code.
- Simplified maintenance: The generated code follows the same structure, making it easier to manage.
Setting up Pigeon in Flutter
Let's walk through the steps to set up and use Pigeon for native communication.
Step 1: Add Pigeon as a dependency
Add Pigeon to your dev_dependencies
in the pubspec.yaml
file:
dev_dependencies:
pigeon: [version]
Then, run flutter pub get
to install the package.
Step 2: Define a Pigeon file
Pigeon requires you to define the communication interface in a Dart file. This is where you specify the methods and data types that will be shared between Dart and the native platforms. Flutter recommends to create a pigeon file in the pigeons
folder at the root of your project.
Create a file named pigeon.dart (or any other name), and inside this file, define the classes and methods that will be used for communication:
import 'package:pigeon/pigeon.dart';
class Request {
int? input;
}
class Response {
int? result;
}
()
abstract class CalculatorApi {
Response calculate(Request request);
}
In this example:
- We define two classes (
Request
andResponse
) to represent the data we’ll be passing. - We define an interface
CalculatorApi
annotated with@HostApi()
to indicate that this is the API that will be implemented on the native side.
Step 3: Generate the code
You can generate the platform-specific code using the Pigeon tool. Run the following command:
dart run pigeon \
--input pigeons/pigeon.dart \
--dart_out lib/pigeon.g.dart \
--kotlin_out android/app/src/main/kotlin/com/example/app/Pigeon.g.kt \
--kotlin_package "com.example.app" \
--swift_out ios/Runner/Pigeon.g.swift
This will create:
- pigeon.g.dart in the Flutter lib folder for Dart code.
- Pigeon.g.kt in the Android project for Kotlin code.
- Pigeon.g.swift in the iOS project for Swift code.
Step 4: Implement native code
Android (Kotlin)
Register the API in your MainActivity.kt
:
package com.example.app
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import com.example.app.Pigeon
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
CalculatorApi.setUp(flutterEngine.dartExecutor.binaryMessenger, object : CalculatorApi {
override fun calculate(request: Request): Response {
val response = Pigeon.Response()
response.result = request.input?.times(2) // Example: multiply input by 2
return response
}
})
}
}
iOS (Swift)
Register the API in your AppDelegate.swift
:
import Flutter
import UIKit
private class CalculatorApiImpl: CalculatorApi {
func calculate(request: Request) -> Response {
let response = Response()
response.result = request.input! * 2 // Example: multiply input by 2
return response
}
}
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
let controller = window?.rootViewController as! FlutterViewController
CalculatorApi.setUp(binaryMessenger: controller.binaryMessenger, api: CalculatorApiImpl())
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Step 5: Call native code from Flutter
Now, in Flutter, you can call the native code using the generated CalculatorApi
:
import 'pigeon.dart';
void calculate() async {
final api = CalculatorApi();
final request = Request()..input = 10;
final response = await api.calculate(request);
print('Result: ${response.result}');
}
In this example, we send an input value of 10
, and the native platforms (Kotlin and Swift) return the result of multiplying that value by 2
.
Enter pigeon_generator
By using Pigeon, you can greatly simplify native communication in Flutter applications. It removes the need for manually writing platform channels and instead provides a strongly-typed, easy-to-maintain solution. Pigeon-generated code ensures that both the Dart and native code are in sync, reducing the chances of errors.
However, when working with multiple files, it can be challenging to keep track of all the generated files and having to manually call dart run pigeon
for each file can be time-consuming. This also becomes an issue everytime we make changes to the Pigeon file.
To simplify this process, I created a package called pigeon_generator that integrates Pigeon with build_runner. This package allows us to run dart run build_runner build
to generate the platform-specific code, and dart run build_runner watch
to automatically run the generator whenever the Pigeon file is modified.
Using pigeon_generator
Let's walk through the steps to set up and use pigeon_generator.
Step 1: Add pigeon_generator as a dependency
Add pigeon_generator and the other dependencies to your dev_dependencies
in the pubspec.yaml
file:
dev_dependencies:
build_runner: [version]
pigeon: [version]
pigeon_generator: [version]
Then, run flutter pub get
to install the packages.
Step 2: Create a pigeons folder
Create a folder named pigeons
in the root of your project. This folder will contain all the pigeon files.
You willl also need to include this folders in the build.yaml
file so that the build_runner
can pick up the pigeon files.
additional_public_assets:
- pigeons/**
You may use a different folder other than pigeons
but you will need to update the build.yaml
file accordingly. If you are using a different folder, you will have to specify that folder in the build.yaml file options for the pigeon_generator
builder. Example is as shown below:
targets:
$default:
builders:
pigeon_generator:
options:
inputs: pigeons_other
additional_public_assets:
- pigeons_other/**
Step 3: Run the generator
Run dart run build_runner build
to generate the platform-specific code. This will create the necessary files in the lib
folder for Dart and the native platforms.
To automatically run the generator whenever the Pigeon file is modified, run dart run build_runner watch
.
That's all you need to do! The generator will pick up which platforms your project supports by checking for the existence of the platform's folder in the root project and generate the necessary files. You can however specify custom options by passing them to the build.yaml
file. To learn more, check out the pigeon_generator package.
Conclusion
We've seen how Pigeon by itself can greatly simplify native communication in Flutter applications. Using it with pigeon_generator takes it a step further by automating the process of generating the platform-specific code thanks to build_runner. This makes it easier to work with multiple files without having to manually type dart run pigeon
for each file. It also utilizes the watch
feature of build_runner to automatically run the generator whenever the Pigeon file is modified, ensuring that the generated code is always up-to-date.