skip to content

In Flutter, how does an EventChannel stream step counts from host code, and when does the host start and stop producing events?

level: middleimportance: should knowfreq 38%

answer

  1. receiveBroadcastStream on the Dart side
  2. listen and cancel method calls
  3. StreamHandler onListen / onCancel
  4. EventSink success, error, endOfStream
  5. first listener in, last listener out

basics

~20 s

Dart 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 s

An `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 lines
kotlin
EventChannel(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

for a junior

Recall that an EventChannel gives Dart a Stream from host code, and that you listen to it and cancel the subscription when done.

for a middle

Explain the listen and cancel handshake, how host errors become PlatformException events, and what endOfStream does to the Dart stream.

for a senior

Talk about battery cost of forgotten subscriptions, sharing one broadcast stream, the main-thread requirement for EventSink and tolerating onCancel(null) after hot restart.

for a principal

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