skip to content

In Dart, what does `asBroadcastStream()` do with the underlying subscription as listeners come and go, and when do its `onListen` and `onCancel` callbacks matter?

level: seniorimportance: should knowfreq 28%

answer

  1. subscribes on the first listener
  2. stays subscribed with zero listeners
  3. events with no listener are lost
  4. onCancel: pause or cancel the source
  5. cancelling there ends it for everyone

basics

~20 s

asBroadcastStream subscribes to the source when its first listener arrives and stays subscribed until the source ends or a callback cancels it, even with no listeners, so events fired meanwhile are lost. onListen and onCancel let you pause, resume or cancel the source.

solid answer

~40 s

`asBroadcastStream()` returns a broadcast stream that **subscribes to the source when its first listener arrives** and, by default, **stays subscribed until the source ends**, even after every listener has cancelled. So the source keeps running, and events it produces while nobody is listening are dropped; a later listener sees only what comes next. The optional `onCancel` callback runs when the last listener leaves and receives a subscription-like handle to the source, so you can `pause()` it to stop losing events, or `cancel()` it to free the resource. `onListen` runs again when a listener returns, and is where you `resume()`. Cancel there only when nobody is listening: cancelling the source stops all current subscribers without even a done event.

go deeper

for a junior

Know that asBroadcastStream lets several listeners share a stream that would otherwise allow only one.

for a middle

Explain that it subscribes on the first listener, stays subscribed by default, and that events fired with no listeners are lost.

for a senior

Use onCancel and onListen to pause, resume or cancel the source deliberately, and avoid the cancel-while-listening trap that silences current subscribers.

for a principal

Decide where sharing belongs: adapting at call sites with asBroadcastStream, or making the owning layer expose broadcast streams with a documented lifecycle.

## What the adapter is for A single-subscription stream, such as one returned by an `async*` function, accepts one listener. When several parts of an app need the same events, for example a temperature display and a logger reading one sensor, `asBroadcastStream()` wraps it in a **broadcast stream** that many listeners can share, while the source itself still has exactly one subscriber: the adapter. ## The default lifecycle The SDK documents the behaviour precisely: 1. The adapter **subscribes to the source when its first listener is added**. Creating it does nothing on its own. 2. It then **stays subscribed until the source ends**, or until one of the callbacks cancels that subscription. 3. Listeners can come and go freely; each sees the events fired while it is subscribed. Point 2 is the surprise. With the defaults, when every listener cancels, the adapter keeps the source subscription open. The sensor keeps being polled, and because a broadcast stream fires events **whether there are listeners or not**, those readings are **lost**. When a listener returns, it picks up from the next reading. For a sensor that is often fine; for a file or a network body it wastes the resource and loses data. ## Three states to reason about It helps to think of the adapter as moving between three states: 1. **Never listened to**: the source is not subscribed; the sensor generator has not started. 2. **One or more listeners**: the source is subscribed and every event is delivered to the current listeners. 3. **All listeners gone**: with the defaults, the source is **still subscribed**, and each event is fired to nobody and lost. State 3 is where the callbacks come in. The adapter never returns to state 1 on its own; only the source ending, or a callback cancelling it, releases the underlying subscription. ## Controlling it with `onListen` and `onCancel` The signature is `Stream<T> asBroadcastStream({void onListen(StreamSubscription<T> subscription)?, void onCancel(StreamSubscription<T> subscription)?})`: - **`onCancel`** is called when the broadcast stream **stops having listeners**. - **`onListen`** is called when it gets a listener, including when a listener arrives again after `onCancel` ran. - Both receive a **subscription-like object for the underlying source subscription**. You can `pause`, `resume` or `cancel` it inside the callback, but you cannot replace its event handlers or call `asFuture` on it. Two common policies: | Goal | `onCancel` | `onListen` | |---|---|---| | Do not lose events while nobody listens | `sub.pause()` | `if (sub.isPaused) sub.resume()` | | Free the source when nobody listens | `sub.cancel()` | nothing: the source cannot restart | Pausing works because a paused subscription **buffers** events it receives, and a non-broadcast source is usually told to stop producing; an `async*` source pauses at its next `yield`. The cost of pausing is memory if the source keeps pushing regardless. ## The cancel trap The SDK warns that cancelling is intended for when there are **no current subscribers**. If the source subscription passed to a callback is cancelled, **no further events are ever emitted** by current subscriptions on the broadcast stream, **not even a done event**. Code awaiting `broadcast.first` or an `await for` loop over it would simply wait forever. So cancel only from `onCancel`, never from `onListen` while others are listening. ## Example ```dart final shared = readings().asBroadcastStream( onCancel: (sub) => sub.pause(), onListen: (sub) { if (sub.isPaused) sub.resume(); }, ); final display = shared.listen((t) => print('display: $t')); final logger = shared.listen((t) => print('log: $t')); // later await display.cancel(); await logger.cancel(); // last listener gone: onCancel pauses the sensor ``` ## Choosing between this and alternatives - If each consumer can have its own source, calling the producer twice avoids sharing entirely. - If the source's owner can expose a broadcast stream directly, that is cleaner than adapting at every call site. - If late listeners must see the **latest value** on subscription, a plain broadcast stream will not do that; it needs a replaying design built for it. `asBroadcastStream` is the right tool when you have a single-subscription stream you do not own and several consumers that are happy to see only what happens while they listen.

  • Why might the sensor keep being polled after both listeners cancel?
    Because by default the adapter stays subscribed to the source until the source ends. With no `onCancel` callback, cancelling every listener leaves the underlying subscription active, so the `async*` generator keeps running and its readings are dropped. Pass `onCancel: (sub) => sub.pause()` or `sub.cancel()` to change that.
  • What happens to current listeners if `onListen` cancels the source subscription?
    They stop receiving anything. The SDK documents that cancelling the subscription handed to `onListen` or `onCancel` means no further events are emitted on current subscriptions, not even a done event, so an `await for` over the broadcast stream would wait forever. Cancel only when there are no listeners.

saying these in an interview costs you the question

  • asBroadcastStream cancels the source when the last listener leaves
  • asBroadcastStream subscribes to the source as soon as it is called
  • Events are buffered for listeners who join later
  • Cancelling the source in a callback sends done to current listeners
  • The callbacks can replace the source subscription's event handlers