类型安全的平台通道与 Pigeon
使用 Pigeon 工具生成强类型的主机端与 Flutter 消息传递接口。
类型安全的平台通道与 Pigeon 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 2 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 Flutter Mobile Development 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 Flutter Mobile Development 课程共包含 4 节课。
本课时的部分内容尚未翻译,以英文显示。
What is Pigeon and Why Use It?
Platform channels in Flutter traditionally use string-based method names and untyped dynamic maps. A typo in a method name or a mismatched argument type only fails at runtime — often on a device you don't own.
Pigeon is a code-generation tool from the Flutter team that solves this problem. You define a Dart API file describing the messages and host APIs, and Pigeon generates:
- Type-safe Dart classes and abstract channel stubs
- Matching native code for Android (Kotlin/Java) and iOS (Swift/ObjC)
The result is a compile-time-checked contract between Flutter and the host platform — no raw strings, no untyped maps.
Adding Pigeon to Your Project
Pigeon is a dev-only dependency. Add it to pubspec.yaml under dev_dependencies:
You also need the Flutter standard method codec, which Pigeon uses internally — it is already part of the Flutter SDK, so no extra package is required.
After adding the dependency, run flutter pub get. Pigeon is invoked via dart run pigeon (not as a build_runner builder), which gives you full control over when code is regenerated.
# pubspec.yaml (relevant excerpt)
# dev_dependencies:
# pigeon: ^22.0.0
// Run generation (in terminal, not Dart code):
// dart run pigeon --input pigeons/messages.dartDefining a Pigeon API File
A Pigeon API file is plain Dart, decorated with special annotations. Place it in a pigeons/ folder at the project root — it is never compiled into your app; it is only read by the generator.
Key annotations:
@ConfigurePigeon— declares output paths for each platform@HostApi()— Flutter calls native (platform implements)@FlutterApi()— Native calls Flutter (Flutter implements)@EventChannelApi()— Streaming events from native to Flutter
All data classes are plain Dart classes with typed fields — no dynamic anywhere.
// pigeons/messages.dart
import 'package:pigeon/pigeon.dart';
@ConfigurePigeon(PigeonOptions(
dartOut: 'lib/src/messages.g.dart',
dartOptions: DartOptions(),
kotlinOut:
'android/app/src/main/kotlin/com/example/app/Messages.g.kt',
kotlinOptions: KotlinOptions(),
swiftOut: 'ios/Runner/Messages.g.swift',
swiftOptions: SwiftOptions(),
))
// Data class shared between Flutter and native
class BatteryInfo {
BatteryInfo({required this.level, required this.isCharging});
final int level;
final bool isCharging;
}
// Flutter calls native to read battery
@HostApi()
abstract class BatteryHostApi {
BatteryInfo getBatteryInfo();
@async
BatteryInfo getBatteryInfoAsync();
}
// Native calls Flutter to report a low-battery event
@FlutterApi()
abstract class BatteryFlutterApi {
void onLowBattery(BatteryInfo info);
}Running the Generator
Once the API file is ready, generate platform code with a single command:
dart run pigeon --input pigeons/messages.dartPigeon reads the @ConfigurePigeon options and writes three files:
- lib/src/messages.g.dart — Dart channel wrapper + data classes
- android/.../Messages.g.kt — Kotlin interface + registration helpers
- ios/Runner/Messages.g.swift — Swift protocol + setup call
These .g.dart / .g.kt / .g.swift files are committed to source control (unlike build_runner outputs that are sometimes gitignored) because they are stable, reviewable platform code.
Re-run the command whenever you change the API file. CI should fail if the generated files are out of sync with the source.
Implementing the Host API on Android (Kotlin)
After generation, Pigeon gives you a Kotlin interface that mirrors your @HostApi definition. Your MainActivity (or a plugin class) implements it and registers it on the binary messenger.
Key points:
- The generated interface is named exactly as your Dart abstract class
- Registration call is
BatteryHostApi.setUp(binding.binaryMessenger, impl) - Async methods receive a
Result<T>callback instead of returning directly
// android/app/src/main/kotlin/com/example/app/MainActivity.kt
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
class MainActivity : FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
BatteryHostApi.setUp(
flutterEngine.dartExecutor.binaryMessenger,
BatteryHostApiImpl()
)
}
}
class BatteryHostApiImpl : BatteryHostApi {
override fun getBatteryInfo(): BatteryInfo {
// Real impl would query BatteryManager
return BatteryInfo(level = 87L, isCharging = true)
}
override fun getBatteryInfoAsync(result: Result<BatteryInfo>) {
// Offload to coroutine in production
result.success(BatteryInfo(level = 87L, isCharging = true))
}
}Implementing the Host API on iOS (Swift)
On iOS, Pigeon generates a Swift protocol. Your AppDelegate (or a Flutter plugin) conforms to it and registers via the generated setup function.
Notice how the generated Swift API matches the Kotlin API structurally — Pigeon enforces this symmetry so both platforms honour the same contract.
// ios/Runner/AppDelegate.swift
import Flutter
import UIKit
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions options: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller = window?.rootViewController as! FlutterViewController
let messenger = controller.binaryMessenger
BatteryHostApiSetup.setUp(
binaryMessenger: messenger,
api: BatteryHostApiImpl()
)
return super.application(application,
didFinishLaunchingWithOptions: options)
}
}
class BatteryHostApiImpl: BatteryHostApi {
func getBatteryInfo() throws -> BatteryInfo {
// UIDevice.current.batteryLevel in production
return BatteryInfo(level: 87, isCharging: true)
}
func getBatteryInfoAsync(
completion: @escaping (Result<BatteryInfo, Error>) -> Void
) {
completion(.success(BatteryInfo(level: 87, isCharging: true)))
}
}Calling the Host API from Dart
On the Flutter side, the generated code gives you a concrete class — not an abstract one. You simply instantiate it and call methods as regular async Dart:
- No channel name strings to type
- No
invokeMethodcalls - No manual argument packing/unpacking
- Full type safety — the return type is
BatteryInfo, notdynamic
Errors thrown by the native side are surfaced as PlatformException — catch them normally.
// lib/battery_page.dart
import 'package:flutter/material.dart';
import 'src/messages.g.dart'; // generated
class BatteryPage extends StatefulWidget {
const BatteryPage({super.key});
@override
State<BatteryPage> createState() => _BatteryPageState();
}
class _BatteryPageState extends State<BatteryPage> {
final _api = BatteryHostApi(); // generated class
BatteryInfo? _info;
Future<void> _fetch() async {
try {
// Typed return — no casts needed
final info = await _api.getBatteryInfoAsync();
setState(() => _info = info);
} on PlatformException catch (e) {
debugPrint('Native error: ${e.message}');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Battery')),
body: Column(
children: [
Text('Level: ${_info?.level ?? '--'}%'),
Text('Charging: ${_info?.isCharging ?? '--'}'),
ElevatedButton(
onPressed: _fetch,
child: const Text('Refresh'),
),
],
),
);
}
}Implementing FlutterApi — Native Calls Flutter
@FlutterApi reverses the direction: native code calls into Flutter. Pigeon generates a concrete class on the native side that you instantiate and call, while Flutter provides an implementation of the generated abstract interface.
A common use case is push-notification delivery: the native SDK receives the notification and calls Flutter to update the UI without Flutter polling.
On the Dart side, register the Flutter implementation using the generated setUp method:
// lib/src/battery_flutter_api_impl.dart
import 'messages.g.dart';
class BatteryFlutterApiImpl extends BatteryFlutterApi {
// Called by native when battery drops below threshold
@override
void onLowBattery(BatteryInfo info) {
debugPrint(
'LOW BATTERY: ${info.level}% '
'(charging: ${info.isCharging})',
);
// Update state, show snackbar, etc.
}
}
// Register once — e.g. in main() or an initState
void registerFlutterApis() {
BatteryFlutterApi.setUp(BatteryFlutterApiImpl());
}Nullable Fields and Enum Support
Pigeon data classes support nullable fields and Dart enums natively. Enums are serialised as integers over the wire — Pigeon generates the mapping code on all sides so you never deal with raw integers in your business logic.
- Nullable fields become platform-native optionals (Swift
Optional, Kotlin?) - Enums become sealed Kotlin enum classes and Swift enum types
- Lists and Maps of typed values are supported (
List<String>,Map<String, int>)
// pigeons/messages.dart (additions)
enum ChargingStatus { unknown, charging, discharging, full }
class DetailedBatteryInfo {
DetailedBatteryInfo({
required this.level,
required this.status,
this.temperature, // nullable — not available on all devices
this.voltages,
});
final int level;
final ChargingStatus status;
final double? temperature;
final List<double>? voltages;
}
@HostApi()
abstract class DetailedBatteryHostApi {
@async
DetailedBatteryInfo getDetailedInfo();
}Error Handling with PigeonError
Pigeon propagates native errors to Dart as structured exceptions. On the native side you throw a FlutterError (iOS) or use result.error() (Android async). On the Dart side this surfaces as a PlatformException with typed fields.
For richer error contracts, Pigeon also supports a dedicated error class pattern: define a class annotated with nothing special — just return it as a Result error. This lets you send structured error payloads (code + details) instead of bare strings.
// pigeons/messages.dart
class BatteryError {
BatteryError({required this.code, this.details});
final String code; // e.g. 'PERMISSION_DENIED'
final String? details;
}
// Dart call-site
Future<void> safeFetch() async {
try {
final info = await BatteryHostApi().getBatteryInfoAsync();
print('Level: ${info.level}');
} on PlatformException catch (e) {
// e.code = native error code string
// e.message = human-readable message
// e.details = arbitrary details object
if (e.code == 'PERMISSION_DENIED') {
print('Need battery permission');
} else {
rethrow;
}
}
}Streaming with EventChannelApi
For continuous streams (sensor data, connectivity changes, etc.) Pigeon 14+ introduced @EventChannelApi(). It generates a typed Stream<T> on the Dart side backed by a real EventChannel — no manual codec work needed.
- Define the data type as a normal Pigeon data class
- Native implements
StreamHandler(Kotlin) /FlutterStreamHandler(Swift) - Dart consumes a plain
Stream— works withStreamBuilder,listen,async for
// pigeons/messages.dart
@EventChannelApi()
abstract class BatteryEventApi {
// The return type defines what flows down the stream
BatteryInfo onBatteryChanged();
}
// lib/battery_stream_page.dart
import 'src/messages.g.dart';
class BatteryStreamPage extends StatelessWidget {
const BatteryStreamPage({super.key});
@override
Widget build(BuildContext context) {
// BatteryEventApi().onBatteryChangedStream() returns Stream<BatteryInfo>
return StreamBuilder<BatteryInfo>(
stream: BatteryEventApi().onBatteryChangedStream(),
builder: (context, snapshot) {
if (!snapshot.hasData) return const CircularProgressIndicator();
final info = snapshot.data!;
return Text('${info.level}% – charging: ${info.isCharging}');
},
);
}
}Knowledge Check: Pigeon API Direction
Which Pigeon annotation should you use when you want native platform code to call into Flutter (for example, to push a real-time notification payload into a Dart callback)?
Recap: Type-Safe Platform Channels with Pigeon
In this lesson you learned how Pigeon eliminates the fragility of hand-written platform channels:
- API file — a plain Dart file in
pigeons/annotated with@HostApi,@FlutterApi, or@EventChannelApithat acts as the single source of truth - Code generation —
dart run pigeon --input pigeons/messages.dartproduces type-safe Dart, Kotlin, and Swift in one command - @HostApi — Flutter calls native; native implements the interface and registers it on the binary messenger
- @FlutterApi — Native calls Flutter; Flutter implements the abstract interface via
setUp() - @EventChannelApi — typed
Stream<T>from native to Flutter for continuous data - Rich types — nullable fields, enums, lists, and maps are all supported with platform-native mappings generated automatically
- Error handling — native errors surface as
PlatformExceptionwith structured code and details fields
Pigeon should be your default choice for any new platform channel — the compile-time safety it provides pays for itself the first time it catches a mismatch before you ship.
用 AI 导师学习 Dart — 免费
在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。
- 课程
- 22
- 课程
- 88
常见问题解答
「类型安全的平台通道与 Pigeon」课时是免费的吗?
是的 — 「类型安全的平台通道与 Pigeon」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。
「类型安全的平台通道与 Pigeon」这节课中我会学到什么?
使用 Pigeon 工具生成强类型的主机端与 Flutter 消息传递接口。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。
学习 Flutter Mobile Development 需要有经验吗?
无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 2 节课,共 4 节。
「类型安全的平台通道与 Pigeon」课时需要多长时间?
大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。
我能在这节 Flutter Mobile Development 课中编写并运行代码吗?
能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。
此课程中的所有课时
- 使用 dart:ffi 调用 C 库
- 类型安全的平台通道与 Pigeon
- 为 iOS 和 Android 编写自定义平台插件
- 后台隔离区与原生内存管理