skip to content

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%

answer

  1. method declares the element type
  2. generated top-level function returns Stream
  3. StreamHandler base class with register
  4. PigeonEventSink success / endOfStream
  5. Kotlin, Swift and Dart only

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.

solid answer

~40 s

`@EventChannelApi()` marks an abstract class whose methods each become an event stream. You declare the element type, not `Stream`: `ScanEvent scans();`. Pigeon then generates a top-level Dart function `Stream<ScanEvent> scans({String instanceName = ''})` built on an `EventChannel` with Pigeon's codec, and in Kotlin and Swift a base class such as `ScansStreamHandler` whose `onListen(arguments, sink)` you override, plus `ScansStreamHandler.register(messenger, handler)`. The sink is a typed `PigeonEventSink<ScanEvent>` with `success`, `error` and `endOfStream`. A common pattern is an empty `sealed class ScanEvent` with subclasses such as `BarcodeFound` and `ScannerError`, which Dart consumes with an exhaustive `switch`. Event channels are supported only by the Kotlin, Swift and Dart generators.

code

kotlin · 16 lines
kotlin
class ScanStream(private val sdk: ScannerSdk) : ScansStreamHandler() {
  private var sink: PigeonEventSink<ScanEvent>? = null

  override fun onListen(p0: Any?, sink: PigeonEventSink<ScanEvent>) {
    this.sink = sink
    sdk.startContinuous { text -> sink.success(BarcodeFound(rawValue = text, format = BarcodeFormat.QR)) }
  }

  override fun onCancel(p0: Any?) {
    sdk.stopContinuous()
    sink = null
  }
}

// In configureFlutterEngine:
ScansStreamHandler.register(flutterEngine.dartExecutor.binaryMessenger, ScanStream(sdk))

go deeper

for a junior

Recall that @EventChannelApi turns a declared element type into a Dart Stream fed by host code.

for a middle

Explain the generated Dart function, the host stream-handler base class and register call, and the typed sink methods.

for a senior

Use sealed event hierarchies for exhaustive handling, release native resources in onCancel, and use instanceName for parallel streams.

for a principal

Decide whether a plugin's streaming surface is worth Pigeon's Kotlin/Swift-only support or should stay a hand-written channel for wider platform reach.

## What @EventChannelApi is for A **`@HostApi`** method gives one reply per call. A barcode scanner in continuous mode produces a **stream** of results for as long as the camera is open. Before Pigeon supported streams, plugins mixed typed Pigeon calls with a hand-written `EventChannel` and untyped events. **`@EventChannelApi()`** brings streams into the same generated, typed contract. ## Writing the definition ```dart import 'package:pigeon/pigeon.dart'; sealed class ScanEvent {} class BarcodeFound extends ScanEvent { BarcodeFound({required this.rawValue, required this.format}); String rawValue; BarcodeFormat format; } class ScannerStopped extends ScanEvent { ScannerStopped({required this.reason}); String reason; } @EventChannelApi() abstract class ScannerEvents { ScanEvent scans(); } ``` Rules from the README: - Wrap stream methods in an `abstract class` annotated `@EventChannelApi()`. - Declare the **element type** only; the generator adds the `Stream`. - Event channels are generated only for **Kotlin, Swift and Dart**, not Java, Objective-C, C++ or GObject. - Basic inheritance through an **empty `sealed` parent class** is allowed only in the Swift, Kotlin and Dart generators, which suits event unions. ## What is generated | Side | Output | |---|---| | Dart | `Stream<ScanEvent> scans({String instanceName = ''})`, a top-level function wrapping `EventChannel(...).receiveBroadcastStream()` with Pigeon's method codec | | Kotlin | `abstract class ScansStreamHandler` with `onListen(p0: Any?, sink: PigeonEventSink<ScanEvent>)` and `onCancel`, plus `ScansStreamHandler.register(messenger, handler)` | | Swift | a `ScansStreamHandler` class to subclass with `onListen(withArguments:sink:)`, plus `register(with:streamHandler:)` | The `@EventChannelApi` class itself is not generated in Dart; only its methods become functions. `EventChannelApi` also accepts `kotlinOptions` and `swiftOptions` for per-API tweaks. ## Consuming it in Dart ```dart StreamSubscription<ScanEvent>? _sub; void startScanning() { _sub = scans().listen((ScanEvent e) { switch (e) { case BarcodeFound(:final String rawValue): _onBarcode(rawValue); case ScannerStopped(:final String reason): _onStopped(reason); } }); } void stopScanning() => _sub?.cancel(); ``` Because `ScanEvent` is sealed in the generated Dart code, the `switch` is checked for exhaustiveness: add a new event class to the definition and every consumer that does not handle it fails to compile. ## Host side 1. Subclass the generated handler and keep the sink from `onListen`. 2. Push `BarcodeFound(...)` with `sink.success(...)` for each hit, and `sink.endOfStream()` when the session ends. 3. Release the camera in `onCancel`, which runs when the last Dart listener cancels. 4. Register once with `ScansStreamHandler.register(binaryMessenger, handler)`. The underlying mechanics are those of a plain `EventChannel`: activation on the first listener, cancellation after the last, and the host-side sink used from the main thread. ## Instances `instanceName` on the Dart function and on the host `register` call appends a suffix to the channel name, so two scanners (front and back camera) can stream independently. ## Compared with a hand-written EventChannel | Concern | Hand-written `EventChannel` | `@EventChannelApi` | |---|---|---| | Event type in Dart | `dynamic`, cast by hand | typed `Stream<ScanEvent>` | | Host sink | untyped `EventSink` | `PigeonEventSink<ScanEvent>` | | Channel name | a string both sides repeat | generated from package, API and method | | Codec for custom classes | write your own subclass | generated with the request-reply APIs | | Platforms | every embedding | Kotlin, Swift and Dart generators only | ## Pitfalls worth naming - **Forgetting `register`.** Without it the first Dart listener's activation fails, and that failure is reported through Flutter's error reporting rather than as a stream error, so the stream simply stays silent. - **Doing nothing in `onCancel`.** The camera and the SDK session keep running after the screen closes, draining the battery. - **Several `scans()` calls on one instance name.** Each call builds a new stream on the same channel; share one stream through a repository rather than calling the function from every widget. - **Changing the event hierarchy without regenerating the host.** The Dart side decodes type bytes it expects; regenerate all outputs together.

  • Why declare the element type rather than Stream<ScanEvent> in the definition?
    The README requires it: every method in an `@EventChannelApi` class already means 'a stream of this type', and the generator adds `Stream` in the Dart output and a typed `PigeonEventSink` on the host. Writing `Stream` yourself is not part of the definition language.
  • What happens if a Java-based plugin needs @EventChannelApi?
    It cannot generate it: event channels are supported only by the Kotlin, Swift and Dart generators. A Java plugin either moves that handler to Kotlin or falls back to a hand-written `EventChannel` for the stream while keeping Pigeon for the request-reply methods.

saying these in an interview costs you the question

  • Declares the method as returning Stream in the definition file
  • Expects @EventChannelApi output for Java or Objective-C
  • Leaves the camera running because onCancel does nothing
  • Believes the generated Dart API is a class you instantiate