A Flutter plugin bridges a barcode-scanning SDK with Pigeon; which Pigeon-specific failures appear in production, and how do you prevent them?
answer
- same Pigeon version on both sides
- channel-error, not MissingPluginException
- null-error for a non-null return
- do not split generated code across packages
- keep generated types out of public API
basics
~20 sThe 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.
solid answer
~40 sFirst, **version skew**: the README says both sides must be generated by the same Pigeon version, so generated Dart in a platform-interface package and host code in an implementation package can crash after an update; keep both outputs in one package and regenerate together. Second, **missing registration**: if the host never called `ScannerHostApi.setUp` on this engine's messenger, the generated Dart code throws `PlatformException(code: 'channel-error')` rather than `MissingPluginException`, so error handling written for hand-made channels misses it. Third, **nullability drift**: a host returning null for a non-null return type yields `'null-error'`. Fourth, **public API leakage**: generated code changes shape between releases, so expose your own Dart types and keep Pigeon classes internal. Also check `dartPackageName` and `messageChannelSuffix` when two copies of an API must coexist, and regenerate in CI to catch stale files.
go deeper
Recall that both generated halves must come from the same definition and Pigeon version, and that errors reach Dart as PlatformException.
Explain channel-error and null-error, and why a missing setUp shows up as a PlatformException instead of MissingPluginException.
Show how you would structure the plugin so generated code never splits across packages or leaks into the public API, and how CI catches stale output.
Weigh Pigeon's churn against its safety for a long-lived plugin, and decide how generated contracts are versioned relative to the plugin's own semver.
## Why Pigeon has its own failure modes Pigeon removes the stringly-typed bugs of hand-written channels, but it adds a **build artefact** shared by two languages. Most production failures come from the two halves drifting apart, or from assumptions carried over from hand-written `MethodChannel` code. ## 1. Version skew between the two halves The Pigeon README is blunt: the message format is an internal detail that changes without being called a breaking change, and **both sides must be generated with the same version**; mixing versions has undefined behaviour, including crashes. It also warns that generated code **should not be split across packages**, for example Dart output in a platform-interface package and host output in an implementation package, because users can upgrade one without the other. Prevention: - Generate Dart and host files together, from one definition, in one package. - Pin Pigeon in `dev_dependencies` and regenerate in CI, failing the build if the committed files differ. ## 2. Registration that never happened Generated Dart code does not use `MethodChannel.invokeMethod`; it sends on a `BasicMessageChannel` per method and inspects the reply itself. An empty reply (no handler) becomes: ```dart PlatformException( code: 'channel-error', message: 'Unable to establish connection on channel: "dev.flutter.pigeon.shop_app.ScannerHostApi.scanOnce".', ) ``` So a `try { ... } on MissingPluginException` written for hand-made channels does **not** catch it. Typical causes: - `setUp` is called in an activity or engine other than the one running the Dart code (a second `FlutterEngine`, an add-to-app screen). - `dartPackageName` differs between the Dart and host generation runs, so channel names do not match. - A `messageChannelSuffix` used on one side only. The reverse direction fails the same way: a host call into a `@FlutterApi` whose Dart `setUp` never ran gets a `FlutterError` with code `channel-error`. ## 3. Nullability drift The definition file's nullability is enforced at runtime on the receiving side. If the host returns `null` for a non-null return type, Dart throws `PlatformException(code: 'null-error')`. With Swift, `NSNull` values have been a recurring source of fixes in recent Pigeon releases, which is another reason to keep versions current and identical. | Symptom in Dart | Code | Likely cause | |---|---|---| | `PlatformException` | `channel-error` | no host handler: missing `setUp`, name or suffix mismatch | | `PlatformException` | `null-error` | host returned null for a non-null type | | `PlatformException` | your code, or an exception class name | host threw `FlutterError` / `PigeonError`, or an unexpected exception | ## 4. Generated types in a public API The README strongly discourages exposing Pigeon-generated classes in a package's public API: generated code changes between Pigeon versions, so a Pigeon upgrade would become a breaking change for every user of the plugin. Wrap them: ```dart class Barcode { const Barcode(this.value); final String value; } Future<Barcode?> scan() async { final ScanResult? r = await _api.scanOnce(ScanRequest(format: BarcodeFormat.qr, useTorch: false)); return r == null ? null : Barcode(r.rawValue); } ``` ## 5. Threading assumptions Generated host handlers still run on the platform main thread unless the method is annotated `@TaskQueue(type: TaskQueueType.serialBackgroundThread)`. A blocking SDK call in a synchronous `@HostApi` method stalls that thread exactly as it would in a hand-written handler. ## A checklist 1. One Pigeon version, one package, regenerated in CI. 2. `setUp` called on the messenger of every engine that runs the Dart code. 3. Catch `PlatformException` and branch on `code`, including `channel-error`. 4. Nullability in the definition matches what the SDK can really return. 5. Public Dart API built from your own types.
- Why does code catching MissingPluginException miss a missing Pigeon registration?Generated Dart code sends on a `BasicMessageChannel`, whose `send` returns `null` for an empty reply instead of throwing. The generated helper then throws `PlatformException(code: 'channel-error')` itself, so only a `PlatformException` handler catches it.
- How do you move a slow Pigeon host method off the Android main thread?Annotate the method in the definition with `@TaskQueue(type: TaskQueueType.serialBackgroundThread)` and regenerate; the generated `setUp` then creates a background task queue and registers that method's handler on it. Handlers on that queue run serially, and anything touching main-thread-only APIs must hop back.
saying these in an interview costs you the question
- Puts generated Dart in the platform interface and host code in each implementation package
- Relies on MissingPluginException to detect a missing Pigeon handler
- Exports Pigeon-generated data classes as the plugin's public types
- Declares a return type non-null although the SDK can return nothing
- Assumes Pigeon handlers run on a background thread by default