skip to content

In Dart, why does a second `listen` on a single-subscription stream throw, and how does a broadcast stream behave differently?

level: middleimportance: must knowfreq 70%

answer

  1. one listener for the whole lifetime
  2. Bad state: already been listened to
  3. starts producing only once listened to
  4. broadcast: many listeners, no replay
  5. isBroadcast, and asBroadcastStream to convert

basics

~20 s

A single-subscription Dart stream allows exactly one listener for its whole lifetime, so a second listen throws a StateError, even after the first cancelled. A broadcast stream accepts any number of listeners, but each only sees events emitted after it subscribed.

solid answer

~40 s

Dart has two kinds of stream. A **single-subscription** stream allows one listener during its entire lifetime: it starts producing events only when that listener arrives, delivers everything to it in order, and stops when it cancels. Listening again, even after cancelling, throws a `StateError` ("Stream has already been listened to."). Streams from `async*` functions and file reads are single-subscription, because a late second reader would miss earlier chunks. A **broadcast** stream allows any number of listeners, fires its events whether or not anyone is listening, and a listener only receives events emitted after it subscribed; nothing is replayed. `isBroadcast` tells you which kind you have, transformations such as `where` keep the kind, and `asBroadcastStream()` wraps a single-subscription stream so several listeners can share it.

code

dart · 14 lines
dart
Stream<double> readings() async* {
  var t = 20.0;
  for (var i = 0; i < 3; i++) {
    await Future.delayed(const Duration(milliseconds: 200));
    yield t += 0.5;
  }
}

void main() {
  final shared = readings().asBroadcastStream();
  print(shared.isBroadcast); // true
  shared.listen((t) => print('display: $t'));
  shared.listen((t) => print('log: $t'));
}

go deeper

for a junior

Know that most streams allow only one listener, that a second listen throws a StateError, and that broadcast streams allow many listeners.

for a middle

Explain why single-subscription exists, that broadcast listeners only see remaining events, and how asBroadcastStream or separate calls fix a double listen.

for a senior

Choose between sharing one source and giving each consumer its own, considering lost events for late listeners and duplicated work per subscription.

for a principal

Set conventions for what kind of stream each layer exposes, so consumers never guess whether they may listen twice or will miss earlier events.

## Two kinds of stream A `Stream<T>` in `dart:async` delivers a sequence of **data events**, possibly **error events**, and at most one **done event**. Every stream is one of two kinds, and the kind decides who may listen and when. | | Single-subscription | Broadcast | |---|---|---| | Listeners | exactly one, for the stream's whole lifetime | any number, at any time | | Starts producing | when the listener subscribes | independently of listeners | | Events before a listener exists | not produced yet | fired anyway and lost | | Listening again after cancel | not allowed, throws `StateError` | allowed | | `isBroadcast` | `false` (the default) | `true` | | Typical sources | file reads, `async*` functions, HTTP bodies | UI events, notifications, shared sensors | ## Why the second `listen` throws A **single-subscription** stream is designed for data that only makes sense as a whole, such as chunks of a file. The SDK documents that it allows **a single listener during the whole lifetime of the stream** and that listening twice is not allowed, **even after the first subscription has been cancelled**. Streams backed by a controller enforce this in `listen` itself, throwing `StateError("Stream has already been listened to.")`, which prints as `Bad state: Stream has already been listened to.` The scenario that triggers it is common. A temperature sensor exposes its readings from an `async*` function, and two parts of the app want them, a live display and a logger: ```dart Stream<double> readings() async* { while (true) { await Future.delayed(const Duration(seconds: 1)); yield readSensor(); } } void main() { final temps = readings(); temps.listen((t) => print('display: $t')); temps.listen((t) => print('log: $t')); // StateError: already listened to } ``` The first `listen` starts the generator; the second is rejected at the call. ## Hidden listens The second listener is not always an explicit `listen` call. Convenience members such as `first`, `last`, `toList()`, `length`, `forEach` and an `await for` loop all listen to the stream internally. So on a single-subscription stream, `await temps.first` followed by `await temps.toList()` fails on the second call, exactly like two `listen` calls would. When one stream's events are needed in two ways, either collect them once and reuse the collected values, or make the stream shareable first. ## How broadcast streams behave A **broadcast** stream is for independent events that listeners can join and leave: - It accepts **any number of listeners**, and you may listen again after cancelling. - It **fires events when they are ready, whether there are listeners or not**; events with no listener are simply lost. - A new listener receives **only the remaining events**. `first` on a broadcast stream is the first event *after* you listen, not the first ever emitted. - A listener added while an event is being delivered does not receive that event. - After the done event, new listeners just receive another done event. ## Fixing the sensor There are three honest options, and the choice depends on whether each listener needs its own copy of the source: 1. **Share one source**: `final temps = readings().asBroadcastStream();` then both listeners subscribe to `temps`. One generator runs; both see readings from the moment they subscribed. 2. **One source per listener**: call `readings()` twice. Each call returns a new single-subscription stream, so each listener gets its own generator (and its own sensor polling). 3. **Expose a broadcast stream from the owner of the sensor** in the first place, which is a stream-controller design question. ## Checking and preserving the kind - `stream.isBroadcast` reports the kind; a class extending `Stream` that behaves as broadcast must override it. - Transformations such as `where`, `map` and `skip` return the **same kind** as the stream they are called on, so a `where` over a single-subscription stream is still single-subscription. - `asBroadcastStream()` is the standard adapter from single to broadcast; there is no adapter the other way, because a broadcast stream cannot recover events it already fired. ## What interviewers are testing The question checks whether you know *why* the rule exists (ordered, complete delivery to one consumer) rather than just the error text, and whether you can fix a double-listen without accidentally duplicating work or dropping events a late listener expected to see.

  • Does calling an `async*` function twice give two listeners a way to share one sensor?
    No, it gives each listener its own independent stream. Each call to an `async*` function creates a new single-subscription stream with its own run of the body, so both listeners work, but the sensor is polled twice. To share one run, wrap a single call in `asBroadcastStream()`.
  • Does a `where` applied to a broadcast stream run once per event or once per listener?
    Once per listener. The returned stream is a broadcast stream if the source is, and each subscription to it performs the `test` individually. Expensive per-event work placed in a transformation over a broadcast stream is therefore repeated for every listener.

A single-subscription stream is a sealed parcel addressed to one person: it goes out when they sign for it, and nobody else can open it later. A broadcast stream is a radio station: anyone can tune in or out, but tuning in late never replays what already aired.

saying these in an interview costs you the question

  • Cancelling the first subscription allows a new listen on a single-subscription stream
  • A broadcast stream replays earlier events to late listeners
  • Broadcast streams wait for a listener before firing events
  • Calling where on a single-subscription stream makes it broadcast
  • An async* function returns one shared stream for every caller