Flutter’s strength lies in its ability to create beautiful, performant applications for multiple platforms from a single codebase. However, when you need to interact with platform-specific APIs, you often rely on platform channels. While effective, traditional platform channels can be verbose and error-prone. Pigeon is a tool developed by the Flutter team to simplify and ensure type-safe communication between Flutter and native platform code. This article will guide you through the process of creating and using Pigeon for seamless and type-safe interop with native code in your Flutter applications.
What is Pigeon?
Pigeon is a code generation tool designed to generate type-safe asynchronous message passing code for Flutter and its host platforms (Android and iOS). It removes the boilerplate involved in manually serializing and deserializing data and ensures that your Dart code and native platform code can communicate with each other safely and efficiently.
Why Use Pigeon?
- Type Safety: Ensures that the data passed between Dart and native code is type-safe, reducing runtime errors.
- Code Generation: Automates the creation of boilerplate code, saving time and reducing the risk of manual errors.
- Asynchronous Communication: Simplifies asynchronous message passing, improving performance and responsiveness.
- Clear API Definition: Provides a clear and concise interface definition using a Dart-like syntax.
How to Create Pigeon for Type-Safe Interop in Flutter
Follow these steps to create and use Pigeon in your Flutter project:
Step 1: Add Pigeon Dependency
First, add Pigeon as a dev dependency in your pubspec.yaml
file:
dev_dependencies:
pigeon: ^14.0.0 # Use the latest version
Then, run flutter pub get
to install the dependency.
Step 2: Define the Pigeon Interface
Create a .dart
file to define your Pigeon interface. This interface specifies the methods that can be called from Flutter to native code and vice versa. For example, let’s create pigeon.dart
:
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(
PigeonOptions(
dartOut: 'lib/src/generated/messages.g.dart',
kotlinOut:
'android/src/main/kotlin/com/example/my_app/Messages.g.kt',
swiftOut: 'ios/Runner/Messages.g.swift',
),
)
class ExampleRequest {
String? message;
}
class ExampleResponse {
String? result;
}
@HostApi()
abstract class ExampleApi {
ExampleResponse sendMessage(ExampleRequest request);
}
Explanation:
@ConfigurePigeon
: Configures the Pigeon tool with output paths for the generated Dart, Kotlin, and Swift code.ExampleRequest
andExampleResponse
: Data classes used for passing data between Flutter and native code.@HostApi()
: Defines an interface for methods that can be called from Flutter to native code.ExampleApi
: An abstract class that defines thesendMessage
method.
Step 3: Run Pigeon to Generate Code
Execute the Pigeon code generation tool using the Flutter CLI. Add a script in your `pubspec.yaml`:
scripts:
pigeon: flutter pub run pigeon --input pigeons/messages.dart
Run the Pigeon tool:
flutter pub run pigeon
This command generates the necessary Dart, Kotlin, and Swift files based on your Pigeon interface definition. The generated files are placed in the paths specified in PigeonOptions
.
Step 4: Implement the Native Platform Code
Android (Kotlin)
Implement the ExampleApi
interface in your Kotlin code:
package com.example.my_app
import io.flutter.plugin.common.BinaryMessenger
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class ExampleApiImpl(private val messenger: BinaryMessenger) : ExampleApi {
override fun sendMessage(request: ExampleRequest): ExampleResponse {
val result = "Hello from Kotlin: ${request.message}"
return ExampleResponse().apply {
this.result = result
}
}
}
fun setupExampleApi(messenger: BinaryMessenger): ExampleApi {
val api = ExampleApiImpl(messenger)
ExampleApi.setup(messenger, api)
return api
}
iOS (Swift)
Implement the ExampleApi
protocol in your Swift code:
import Flutter
class ExampleApiImpl: NSObject, ExampleApi {
func sendMessage(request: ExampleRequest, error: AutoreleasingUnsafeMutablePointer) -> ExampleResponse? {
let result = "Hello from Swift: (request.message ?? "")"
let response = ExampleResponse()
response.result = result
return response
}
}
func setupExampleApi(binaryMessenger: FlutterBinaryMessenger) {
let api = ExampleApiImpl()
ExampleApiSetup.setUp(binaryMessenger: binaryMessenger, api: api)
}
Step 5: Register the Native Implementation
Android (Kotlin)
Register the Kotlin implementation in your MainActivity.kt
:
package com.example.my_app
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
setupExampleApi(flutterEngine.dartExecutor.binaryMessenger)
}
}
iOS (Swift)
Register the Swift implementation in your AppDelegate.swift
:
import UIKit
import Flutter
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
setupExampleApi(binaryMessenger: controller.binaryMessenger)
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Step 6: Use the API in Flutter
Call the native method from your Flutter code using the generated API:
import 'package:flutter/material.dart';
import 'src/generated/messages.g.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State {
String _result = 'No result yet.';
@override
void initState() {
super.initState();
_callNativeMethod();
}
Future _callNativeMethod() async {
final api = ExampleApi();
final request = ExampleRequest()..message = 'Hello from Flutter!';
try {
final response = await api.sendMessage(request);
setState(() {
_result = response.result ?? 'No result';
});
} catch (e) {
setState(() {
_result = 'Error: $e';
});
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Pigeon Example'),
),
body: Center(
child: Text(_result),
),
),
);
}
}
Conclusion
Pigeon significantly simplifies the process of interacting with native code in Flutter by providing type safety and automating boilerplate code generation. By following the steps outlined in this article, you can create a robust, efficient, and type-safe bridge between your Flutter and native platform code. This approach not only reduces the risk of runtime errors but also makes your codebase more maintainable and scalable. Pigeon is an excellent tool for any Flutter developer needing to leverage platform-specific functionalities while maintaining a high standard of code quality.