In Dart, why does a second `listen` on a single-subscription stream throw, and how does a broadcast stream behave differently?
answer
- one listener for the whole lifetime
- Bad state: already been listened to
- starts producing only once listened to
- broadcast: many listeners, no replay
- isBroadcast, and asBroadcastStream to convert
basics
~20 sA 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 sDart 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 linesStream<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
Know that most streams allow only one listener, that a second listen throws a StateError, and that broadcast streams allow many listeners.
Explain why single-subscription exists, that broadcast listeners only see remaining events, and how asBroadcastStream or separate calls fix a double listen.
Choose between sharing one source and giving each consumer its own, considering lost events for late listeners and duplicated work per subscription.
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