In Flutter's pigeon package, how do you write a definition file with @ConfigurePigeon, and what does each generated output give the Dart and host sides?
answer
- declarations only, no bodies
- PigeonOptions: dartOut, kotlinOut, swiftOut
- dart run pigeon --input
- ExampleHostApi.setUp on the host
- ints travel as 64-bit
basics
~20 sThe definition file declares data classes, enums and @HostApi/@FlutterApi abstract classes, and @ConfigurePigeon(PigeonOptions(...)) names output paths such as dartOut, kotlinOut and swiftOut; dart run pigeon --input writes a Dart API class and host interfaces registered with setUp.
solid answer
~40 sA Pigeon input file contains only declarations: data classes with typed fields, enums, and abstract classes annotated `@HostApi()` or `@FlutterApi()`. At the top, `@ConfigurePigeon(PigeonOptions(dartOut: ..., kotlinOut: ..., swiftOut: ..., dartPackageName: ...))` records where each output goes, so `dart run pigeon --input pigeons/scanner_api.dart` needs no other flags. The Dart output holds a class such as `ScannerHostApi` whose methods return `Future`s, the data classes and a codec. On Android the Kotlin output holds an interface to implement plus `ScannerHostApi.setUp(binaryMessenger, impl)`, which registers one channel per method; on iOS the Swift output holds a protocol and a setup type. Dart `int`s become `Long` in Kotlin and `Int64` in Swift, because Pigeon's codec always sends them as 64-bit.
code
kotlin · 11 linesprivate class ScannerHostApiImpl(private val sdk: ScannerSdk) : ScannerHostApi {
override fun isSupported(): Boolean = sdk.hasCamera()
override suspend fun scanOnce(request: ScanRequest): ScanResult? {
val hit = sdk.scan(request.format.name, request.useTorch) ?: return null
return ScanResult(rawValue = hit.text, format = request.format)
}
}
// In MainActivity.configureFlutterEngine:
ScannerHostApi.setUp(flutterEngine.dartExecutor.binaryMessenger, ScannerHostApiImpl(sdk))go deeper
Recall the pieces of a definition file and that one command writes both the Dart and host files.
Explain @ConfigurePigeon options, the setUp registration on the host and why generated methods return Futures in Dart.
Discuss one-channel-per-method naming, suffixes for multiple instances, 64-bit ints and how to keep regeneration reproducible.
Set the team rules for where definitions live, how generated code is reviewed, and how Pigeon upgrades are rolled out.
## The input file A **Pigeon definition file** is ordinary Dart that the tool parses rather than runs. The README's rules: - It contains **no method or function bodies**, only declarations. - **Data classes** have fields of supported types (the `StandardMessageCodec` set, other data classes, enums, and typed `List`/`Map`s of those). - **APIs** are `abstract class`es annotated `@HostApi()` (implemented on the host, called from Dart) or `@FlutterApi()` (implemented in Dart, called from the host). - `@ObjCSelector('scan:')` and `@SwiftFunction('scan(_:)')` adjust generated names for Objective-C and Swift. - Top-level `const` `String`, `int`, `double` and `bool` values become constants in every output. ## @ConfigurePigeon and PigeonOptions Rather than passing a dozen command-line flags, put the configuration in the file: ```dart @ConfigurePigeon(PigeonOptions( dartOut: 'lib/src/scanner_api.g.dart', dartPackageName: 'shop_app', kotlinOut: 'android/app/src/main/kotlin/com/example/shop/ScannerApi.g.kt', kotlinOptions: KotlinOptions(), swiftOut: 'ios/Runner/ScannerApi.g.swift', swiftOptions: SwiftOptions(), )) ``` Other `PigeonOptions` fields cover `javaOut`, `objcHeaderOut`/`objcSourceOut`, `cppHeaderOut`/`cppSourceOut`, `gobjectHeaderOut`/`gobjectSourceOut` and `copyrightHeader`. `swiftOut` may be a list, for separate iOS and macOS locations. `dartPackageName` becomes part of every channel name. Then run: ```bash dart run pigeon --input pigeons/scanner_api.dart ``` ## What each output gives you | Side | Generated | You write | |---|---|---| | Dart | `ScannerHostApi` class with `Future`-returning methods; data classes with `encode`/`decode`; a codec extending `StandardMessageCodec` | calls such as `await ScannerHostApi().scanOnce(request)` | | Kotlin | `interface ScannerHostApi` with a companion `setUp(binaryMessenger, api, messageChannelSuffix)`; data and enum classes; `FlutterError` | a class implementing the interface, registered in `configureFlutterEngine` or `onAttachedToEngine` | | Swift | `protocol ScannerHostApi`, a setup type, structs (or classes with `@SwiftClass`), `PigeonError` | a type conforming to the protocol, registered with the engine's messenger | | `@FlutterApi` (Dart) | an abstract class with a static `setUp(api)` | a Dart implementation passed to `setUp` | | `@FlutterApi` (host) | a concrete class wrapping the messenger | calls from host code into Dart | ## Details visible in the generated code 1. **One channel per method.** Each method gets a `BasicMessageChannel` named `dev.flutter.pigeon.<dartPackageName>.<Api>.<method>`, optionally followed by a `messageChannelSuffix` so several instances can coexist. 2. **Arguments as a list.** Dart sends `<Object?>[a, b]`; the host reads `args[0] as Long`. 3. **Ints are always 64-bit.** The generated Dart codec writes every `int` with the 64-bit type byte, so Kotlin sees `Long` and Swift sees `Int64`, unlike a hand-written channel where small ints arrive as 32-bit. 4. **Custom type bytes.** Enums and data classes get their own type bytes above the standard ones, encoded as an index or a list of fields. 5. **Setting `null`.** Passing `null` as the implementation to `setUp` unregisters the handlers. ## Housekeeping - Treat generated files as build outputs: never edit them, regenerate after every definition change. - Pin one Pigeon version for the whole project; the README warns that code generated by different versions on the two sides has undefined behaviour. - Commit the definition file next to the generated outputs, so a reviewer sees the contract change and the regenerated code in the same diff. - Keep one definition file per bridge (scanner, payments, analytics); the tool limits the number of custom types per file, and smaller files regenerate and review faster. - Call `setUp` wherever the engine is created: `configureFlutterEngine` in an app, `onAttachedToEngine` in a plugin, so every engine that runs the Dart code has handlers.
- Why does the generated Kotlin interface take Long where the Dart definition says int?Pigeon's generated codec always encodes a Dart `int` with the 64-bit type byte, so the host always receives a 64-bit value: `Long` in Kotlin, `Int64` in Swift. That removes the hand-written channel trap where small values arrive as `Integer` and large ones as `Long`.
- How do you run two independent instances of the same Pigeon API, for example two scanners?Pass a `messageChannelSuffix` to the Dart constructor and to the host `setUp` call. The suffix is appended to every channel name, so each instance registers and calls its own set of channels without replacing the other's handlers.
saying these in an interview costs you the question
- Writes method bodies in the Pigeon definition file
- Places the definition file under lib/ so it ships with the app
- Expects Kotlin to receive Int for a Dart int parameter
- Hand-edits the generated .g.kt file to add a method
- Generates the Dart and host files with different Pigeon versions