In Dart, how does a StreamController.broadcast() controller behave differently from a default StreamController when events are added?
answer
- one listener versus many
- buffer before listen, or drop
- isPaused always false on broadcast
- second listen throws StateError
- sync: true delivers inside add
basics
~20 sA default controller allows one listener and buffers events added before it subscribes or while it is paused. A broadcast controller allows many listeners, drops events added while nobody listens, and lets each subscription buffer its own pauses.
solid answer
~40 sThe default `StreamController` is single-subscription: a second `listen` throws `StateError('Stream has already been listened to.')`, events added before the listener arrives are buffered and replayed to it, and events added while it is paused are buffered too. `StreamController.broadcast()` hands every current listener each event, has no queue of its own — an `add` with no listeners is simply dropped — and leaves pausing to each subscription, so its `isPaused` is always `false`. It takes only `onListen` (first listener) and `onCancel` (last listener leaves). Both default to `sync: false`, delivering in a later microtask; `sync: true` makes delivery happen inside `add` and is only safe when forwarding an event that is itself already asynchronous.
code
dart · 17 linesimport 'dart:async';
Future<void> main() async {
final single = StreamController<int>();
final broadcast = StreamController<int>.broadcast();
single.add(1); // buffered until the first listener
broadcast.add(1); // dropped: nobody is listening
single.stream.listen((v) => print('single $v'));
broadcast.stream.listen((v) => print('broadcast $v'));
single.add(2);
broadcast.add(2);
await Future<void>.delayed(Duration.zero);
// single receives 1 and 2; broadcast receives only 2
}go deeper
Recall that the default controller has one listener and the broadcast one has many, and that a second listen on the default one throws.
Explain the buffering difference: the default controller replays early and paused events, the broadcast one drops events with no listener and leaves pausing to each subscription.
Diagnose missed first values from late broadcast subscribers and StateError from re-subscribing widgets, and say when sync: true is legitimate.
Decide per stream whether late subscribers need the current value, and make that a documented property of the service rather than an accident of which controller was picked.
## Two controller kinds `dart:async` offers two factories on the same interface: - **`StreamController<T>()`** — its `stream` is a **single-subscription** stream: exactly one listener, ever. - **`StreamController<T>.broadcast()`** — its `stream` is a **broadcast** stream: any number of listeners, and listeners can come and go. What a single-subscription versus a broadcast *stream* means for a consumer is its own topic. The controller-side question interviewers ask is narrower: **what happens to the events you add?** ## Event handling side by side | Situation | Default controller | Broadcast controller | |---|---|---| | `add` before anyone listens | Buffered; replayed to the first listener | **Dropped**; there is no internal queue | | A second `listen` | Throws `StateError('Stream has already been listened to.')` | Allowed; each listener gets later events | | Listener pauses | Controller buffers; `onPause` fires | Only that subscription buffers; others keep receiving | | `isPaused` | `true` with no listener or a paused listener | Always `false` | | Lifecycle callbacks | `onListen`, `onPause`, `onResume`, `onCancel` | `onListen` (first in), `onCancel` (last out) | | `listen` after `close()` | Not possible — the one subscription is spent | Listener receives done immediately | | `close()` future with no listener | Never completes | Does not wait for anyone | Two consequences come up constantly in Flutter code: 1. **Late listeners miss events on broadcast.** If a service adds the current connection state to a broadcast controller in its constructor and a widget subscribes a frame later, that first value is gone. Either replay the latest value yourself on subscription, or keep a separate getter for the current value. 2. **Single-subscription breaks on rebuild-driven relistening.** Handing a default controller's stream to something that subscribes more than once — two widgets, or a widget that re-subscribes after its stream argument changes — throws on the second `listen`. That is the usual reason to reach for `.broadcast()`. ## The sync flag Both factories take `sync` (default `false`). With `sync: false`, `add` only schedules the event and each listener receives it in a later microtask. With `sync: true` the controller is a `SynchronousStreamController`: listeners run **inside** the `add` call. The SDK documents strict rules for that mode: - use it only to forward an event that is already asynchronous — for example from inside another stream's data handler — and add as the last thing that handler does; - never add events from `onListen`, because they would reach the listener before its `listen` call has returned; - on a synchronous **broadcast** controller, `add`, `addError`, `close` and `addStream` must not be called while an event is being delivered. The only benefit is lower latency, so the SDK's advice is: if in doubt, keep it asynchronous. ## Choosing - One consumer that must not lose early events (a request's progress, a file being parsed): **default**. - Several independent observers of something that happens regardless of them (connection status, a chat room's typing notifications): **broadcast**, with an explicit plan for late subscribers. - A stream you already have that is single-subscription but needs a second listener is a consumer-side conversion, `asBroadcastStream`, not a new controller.
- Why does a broadcast controller not support onPause?Several subscriptions share it, and one listener pausing must not stop the others. Each broadcast subscription therefore buffers its own events while paused, the controller keeps delivering to everyone, and its `isPaused` is always `false`. There is no single paused state for the producer to react to.
- When is StreamController(sync: true) appropriate?When you forward an event that is already being handled asynchronously — inside another stream's `onData` — and the `add` is the last thing the handler does. It saves one microtask of latency. Calling it from arbitrary code, or adding in `onListen`, can deliver events before a listener is ready.
saying these in an interview costs you the question
- A broadcast controller buffers events until its first listener subscribes.
- A default StreamController lets any number of widgets listen.
- sync: true is a safe general speed-up for all controllers.
- When one broadcast listener pauses, the controller pauses for everyone.
- Events added before the first listen are lost on a default controller.