skip to content

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%

answer

  1. same Pigeon version on both sides
  2. channel-error, not MissingPluginException
  3. null-error for a non-null return
  4. do not split generated code across packages
  5. keep generated types out of public API

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.

solid answer

~40 s

First, **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

for a junior

Recall that both generated halves must come from the same definition and Pigeon version, and that errors reach Dart as PlatformException.

for a middle

Explain channel-error and null-error, and why a missing setUp shows up as a PlatformException instead of MissingPluginException.

for a senior

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.

for a principal

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