skip to content

Pigeon Codegen

Pigeon generates typed channel code from a Dart API definition: @HostApi for host calls, @FlutterApi for callbacks, @async for futures. Interviewers ask why it beats string method names.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Flutter, what problem does the pigeon package solve compared with a hand-written MethodChannel that matches method names as strings?

level: juniorimportance: should knowfreq 30%

answer

  1. one Dart file describes the API
  2. strings and casts become generated code
  3. typed Dart class plus host interface
  4. same codec, both sides, one run

basics

~20 s

Pigeon generates both ends of a platform channel from one Dart definition file, so channel names, argument order and types are produced by a tool instead of matched by hand, and a mismatch becomes a compile error instead of a runtime failure.

solid answer

~40 s

With a hand-written `MethodChannel`, Dart and host code agree on method names, argument maps and types only by convention: a typo is a `MissingPluginException`, a wrong type a runtime cast failure. Pigeon, a dev dependency, reads a Dart definition file with `@HostApi()` or `@FlutterApi()` abstract classes and data classes, and generates a typed Dart class, a host interface or protocol (Kotlin, Java, Swift, Objective-C, C++ or GObject) with a `setUp` function, data classes and enums on both sides, and a shared codec. Your Kotlin or Swift code just implements the interface, and the Dart side calls ordinary typed methods returning `Future`s. The calls still travel over platform channels, so the asynchronous model and threading rules are unchanged; what disappears is the stringly-typed glue.

code

dart · 32 lines
dart
import 'package:pigeon/pigeon.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(),
))

enum BarcodeFormat { qr, ean13, code128 }

class ScanRequest {
  ScanRequest({required this.format, required this.useTorch});
  BarcodeFormat format;
  bool useTorch;
}

class ScanResult {
  ScanResult({required this.rawValue, required this.format});
  String rawValue;
  BarcodeFormat format;
}

@HostApi()
abstract class ScannerHostApi {
  bool isSupported();

  @async
  ScanResult? scanOnce(ScanRequest request);
}

go deeper

for a junior

Recall that Pigeon generates both sides of a platform channel from one Dart file, so names and types no longer have to be matched by hand.

for a middle

Explain what the definition file contains, what each generated file provides, and that calls remain asynchronous channel messages.

for a senior

Argue when the codegen step pays for itself and how regeneration fits into the build and review process.

for a principal

Decide team-wide whether native bridges use Pigeon by default, weighing typed contracts against tool churn across Pigeon releases.

## The problem with hand-written channels A **platform channel** carries messages between Dart and the **host** (the Kotlin, Swift or C++ side of the app). Written by hand, a `MethodChannel` call relies on three informal contracts: - the **channel name** and **method name** are identical strings on both sides; - the **arguments** are packed into a map or list that both sides read with the same keys and order; - the **result type** the host sends is what the Dart code casts to. Nothing checks these at build time. A renamed method, a changed key, or an `int` read as the wrong width is found by a user, as an exception or a crash. ## What Pigeon does instead **Pigeon** is a code generator published as `package:pigeon`, added as a `dev_dependency`. You describe the interface once, in a Dart file kept outside `lib/`, using only declarations: 1. **Data classes** with fields of supported types, plus **enums**; classes can nest. 2. An **`@HostApi()`** abstract class for methods implemented on the host and called from Dart. 3. Optionally an **`@FlutterApi()`** class for methods implemented in Dart and called from the host, and an **`@EventChannelApi()`** class for streams. Running `dart run pigeon --input pigeons/scanner_api.dart` then writes: | Output | What it contains | |---|---| | Dart file | a class with typed methods returning `Future`s, the data classes with encode/decode, a codec | | Kotlin or Java | an interface to implement, a `setUp(binaryMessenger, api)` companion function, data classes | | Swift or Objective-C | a protocol to conform to, a setup class, structs or classes for the data | | C++ / GObject | an abstract class or vtable for Windows and Linux | ## What stays the same - Messages still go through a `BinaryMessenger` on channels (the generated names look like `dev.flutter.pigeon.<package>.<Api>.<method>`), so every call is **asynchronous from Dart**. - Host handlers still run on the platform main thread unless you choose a background task queue. - The codec is still built on `StandardMessageCodec`, extended with type bytes for your data classes and enums. ## What you gain - **Compile-time checking on both sides.** Change a field in the definition, regenerate, and the Kotlin and Swift implementations stop compiling until they are updated. - **Real data classes** instead of `Map<Object?, Object?>` casts. - **Consistent error mapping.** A host `FlutterError` (Kotlin) or `PigeonError` (Swift) surfaces in Dart as `PlatformException`. - **Fewer handwritten switch statements** routing method names to implementations. ## What it costs - A code-generation step that must be rerun after every definition change, and generated files to keep in version control or regenerate in CI. - Generated code changes shape between Pigeon releases; the README says both sides must be generated with the **same version** and that generated code should not appear in a package's public API. - One more tool for the team to learn, which is not worth it for a single method returning a `bool`. For a bridge to a native barcode-scanning SDK with request and result objects, several methods and callbacks, those costs are usually small next to removing a whole class of runtime-only bugs.

  • Does Pigeon make channel calls synchronous or remove the channel hop?
    No. In its default mode the generated code still sends messages over platform channels, so every call returns a `Future` in Dart and pays for encoding and a message hop. Pigeon 29 adds an experimental native-interop mode over FFI and JNI with synchronous calls, but the standard mode is typed channels.
  • Why is the definition file kept outside lib/?
    It imports `package:pigeon`, a dev dependency, and is input to a generator, not app code. Keeping it in a folder such as `pigeons/` stops it being compiled into the app, while the generated Dart file goes under `lib/` where the app imports it.

saying these in an interview costs you the question

  • Says Pigeon replaces platform channels with a synchronous native call
  • Thinks Pigeon needs no host code at all
  • Believes the Dart definition file ships inside the app
  • Edits generated files by hand instead of the definition
open as a page

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?

level: middleimportance: should knowfreq 26%

basics

~20 s

The 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.

open as a page

In Pigeon for Flutter, how do @HostApi and @FlutterApi differ, and what do @async and @asyncCallback change in the generated Kotlin and Swift signatures?

level: middleimportance: should knowfreq 22%

basics

~20 s

@HostApi methods are implemented on the host and called from Dart; @FlutterApi methods are implemented in Dart and called from the host. Since pigeon 28, @async gives Kotlin suspend functions and Swift async methods, while @asyncCallback keeps completion-callback signatures.

open as a page

A Flutter plugin bridges a barcode-scanning SDK with Pigeon; which Pigeon-specific failures appear in production, and how do you prevent them?

level: seniorimportance: should knowfreq 18%

basics

~20 s

The usual failures are Dart and host files generated by different Pigeon versions, a missing setUp call surfacing as PlatformException 'channel-error', a host returning null for a non-null type ('null-error'), and generated types leaking into a plugin's public API.

open as a page

In Pigeon for Flutter, how does an @EventChannelApi definition stream scanned barcodes from Kotlin or Swift to Dart, and what does it generate?

level: seniorimportance: nice to knowfreq 12%

basics

~20 s

An @EventChannelApi abstract class declares methods whose return type is the streamed element; Pigeon generates a Dart function returning Stream of that type and, on Kotlin and Swift, a stream-handler base class with onListen, a typed PigeonEventSink, and a register function.

open as a page