In Flutter, how does an EventChannel stream step counts from host code, and when does the host start and stop producing events?
answer
- receiveBroadcastStream on the Dart side
- listen and cancel method calls
- StreamHandler onListen / onCancel
- EventSink success, error, endOfStream
- first listener in, last listener out
basics
~20 sDart calls receiveBroadcastStream on an EventChannel; the first listener sends a 'listen' call that runs the host StreamHandler's onListen with an EventSink, and cancelling the last listener sends 'cancel', which runs onCancel to unregister the sensor.
solid answer
~40 sAn `EventChannel` turns a host event source into a Dart broadcast `Stream`. `receiveBroadcastStream(args)` returns the stream lazily: when the listener count goes from 0 to 1, the framework installs a message handler and sends a `listen` method call; the host's `StreamHandler.onListen(arguments, events)` then registers the step-counter sensor and pushes readings with `events.success(count)`. `events.error(code, message, details)` arrives in Dart as a `PlatformException` error event, and `events.endOfStream()` closes the stream. When the listener count drops to 0, Dart sends `cancel` and the host's `onCancel` must unregister the sensor. Errors during activation are reported through `FlutterError`, not the stream. On Android the `EventSink` methods are annotated `@UiThread`, so sensor callbacks from another thread must hop to the main thread first.
code
kotlin · 17 linesEventChannel(messenger, "com.example.app/steps").setStreamHandler(
object : EventChannel.StreamHandler {
private var listener: SensorEventListener? = null
override fun onListen(arguments: Any?, events: EventChannel.EventSink) {
listener = object : SensorEventListener {
override fun onSensorChanged(e: SensorEvent) { events.success(e.values[0].toInt()) }
override fun onAccuracyChanged(s: Sensor?, a: Int) {}
}
sensorManager.registerListener(listener, stepSensor, SensorManager.SENSOR_DELAY_NORMAL)
}
override fun onCancel(arguments: Any?) {
listener?.let { sensorManager.unregisterListener(it) }
listener = null
}
})go deeper
Recall that an EventChannel gives Dart a Stream from host code, and that you listen to it and cancel the subscription when done.
Explain the listen and cancel handshake, how host errors become PlatformException events, and what endOfStream does to the Dart stream.
Talk about battery cost of forgotten subscriptions, sharing one broadcast stream, the main-thread requirement for EventSink and tolerating onCancel(null) after hot restart.
Decide where event streams live in the app architecture so that one owner controls activation and several features share it without duplicate host listeners.
## Why a separate channel for streams A `MethodChannel` gives one reply per call. A pedometer, battery-saver toggles or connectivity changes produce **many values over time**, and the host should only spend energy while someone is listening. `EventChannel` models exactly that: a named channel that exposes a host event source as a Dart **broadcast stream**, with explicit start and stop signals. ## The Dart side ```dart import 'package:flutter/services.dart'; const _stepsChannel = EventChannel('com.example.app/steps'); Stream<int> stepCounts() => _stepsChannel .receiveBroadcastStream(<String, Object?>{'batching': true}) .map((dynamic e) => e as int); ``` What the framework source does inside `receiveBroadcastStream`: 1. Creates a `StreamController.broadcast`. Nothing is sent yet. 2. **`onListen`** (listener count 0 to 1): installs a message handler for the channel name, then sends a method call named `listen` with your arguments over an internal `MethodChannel` of the same name. 3. Each incoming message is decoded as an envelope: a success envelope becomes a **data event**, an error envelope becomes a **`PlatformException` error event**, and an empty message **closes** the stream. 4. **`onCancel`** (listener count 1 to 0): removes the handler and sends `cancel`. 5. If `listen` or `cancel` itself fails, the exception is reported through **`FlutterError.reportError`**, not added to the stream, so subscribers never see an activation failure as an error event. ## The host side The Android and iOS embeddings mirror this with a **stream handler** interface: | Host API | Called when | Your job | |---|---|---| | `onListen(arguments, events)` | Dart sends `listen` | register the sensor, keep the `EventSink` | | `events.success(value)` | each reading | push a codec-supported value | | `events.error(code, message, details)` | a recoverable failure | Dart gets a `PlatformException` event | | `events.endOfStream()` | the source is finished | Dart's stream closes | | `onCancel(arguments)` | Dart sends `cancel` | unregister the sensor, drop the sink | Details from the Android embedding source worth knowing: - The `EventSink` methods are annotated **`@UiThread`**; a sensor callback delivered on another thread must post to the main looper first. - After `endOfStream()`, further `success` or `error` calls are ignored. - If `listen` arrives while a stream is already active (it happens across a hot restart), the channel calls `onCancel(null)` on the old one first, so `onCancel` must tolerate `null` arguments. - An exception thrown from `onListen` is caught and logged, and an error reply is sent back, which the Dart side reports through `FlutterError`. ## Sharing and lifetime rules - The returned stream is **broadcast**: several widgets can listen, and the host sees a single `listen` for the first and a single `cancel` after the last. - Each call to `receiveBroadcastStream` creates a new controller that installs its handler on the **same channel name**. Two independent calls listened to at once fight over that handler; cache one stream (for example in a repository object) and share it. - Arguments are fixed per activation. To change them (a different sampling rate), cancel and listen again. - The subscription is an ordinary Dart `StreamSubscription`: cancel it in `dispose`, or use a `StreamBuilder`, otherwise the sensor keeps running and draining the battery. ## When to reach for something else - A single current value (is battery saver on right now?) is a `MethodChannel` call. - Host-initiated one-off notifications can also be a Dart `setMethodCallHandler`, but then there is no listen/cancel handshake to tell the host when to stop.
- Why might a step-count subscription never report an error even though the host's onListen threw?An exception from `onListen` is caught by the host channel and returned as an error reply to the `listen` call. On the Dart side that failure is passed to `FlutterError.reportError`, not added to the stream, so the subscriber's `onError` never fires. Report recoverable failures through `events.error` instead.
- How do you change the sampling arguments of a running EventChannel stream?Arguments are sent only with the `listen` call. Cancel the current subscription so the host receives `cancel`, then call `receiveBroadcastStream` with the new arguments and listen again. Keep one shared stream per argument set rather than creating several on the same channel name.
An EventChannel is like a radio station that only switches its transmitter on when the first listener tunes in and off when the last one tunes out; the listeners all hear the same broadcast.
saying these in an interview costs you the question
- Says the host starts sending events as soon as the EventChannel is constructed
- Expects activation failures to arrive as stream error events
- Creates a new receiveBroadcastStream per widget on the same channel name
- Never cancels the subscription, leaving the sensor registered
- Believes EventSink.success can be called from any Android thread