In Pigeon for Flutter, how does an @EventChannelApi definition stream scanned barcodes from Kotlin or Swift to Dart, and what does it generate?
answer
- method declares the element type
- generated top-level function returns Stream
- StreamHandler base class with register
- PigeonEventSink success / endOfStream
- Kotlin, Swift and Dart only
basics
~20 sAn @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 linesclass 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
Recall that @EventChannelApi turns a declared element type into a Dart Stream fed by host code.
Explain the generated Dart function, the host stream-handler base class and register call, and the typed sink methods.
Use sealed event hierarchies for exhaustive handling, release native resources in onCancel, and use instanceName for parallel streams.
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