In Dart, when do a StreamController's onListen, onPause, onResume and onCancel callbacks fire, and what should each one do?
answer
- inert until someone listens
- start work in onListen
- stop the source on pause
- onCancel may return a Future
- done also triggers onCancel
basics
~20 sonListen fires when the stream gets its listener and should start the source; onPause and onResume fire as the subscription pauses and resumes and should stop and restart it; onCancel fires when the subscription ends and should release it.
solid answer
~40 sA `StreamController` should stay inert until listened to, so the producer starts in `onListen` — open the socket subscription or start the `Timer` there. `onPause` fires when the subscription goes from running to paused and `onResume` when it can deliver again; use them to pause and resume the source, otherwise the controller buffers everything. `onCancel` fires when the subscription ends — through `cancel()`, and also just before `onDone` runs after the controller is closed — and should tear the source down; it may return a `Future`, which the listener's `cancel()` then returns. Wire all four: when listening and pausing change together, only `onListen` or `onCancel` fires. Broadcast controllers take only `onListen` and `onCancel`.
code
dart · 20 linesimport 'dart:async';
Stream<String> chatLines(Stream<String> socketLines) {
late final StreamController<String> controller;
StreamSubscription<String>? upstream;
controller = StreamController<String>(
onListen: () {
upstream = socketLines.listen(
controller.add,
onError: controller.addError,
onDone: controller.close,
);
},
onPause: () => upstream?.pause(),
onResume: () => upstream?.resume(),
onCancel: () => upstream?.cancel(),
);
return controller.stream;
}go deeper
Remember the four names and the headline rule: start producing in onListen, stop in onCancel.
Explain pause counting, that onResume waits for the buffer to drain, that done also triggers onCancel, and why all four callbacks are needed together.
Show how an unhonoured pause turns a slow listener into unbounded buffering, and when addStream or a transformer forwards pause and cancel for you instead of hand-wired hooks.
Treat stream laziness and pause support as part of a component's resource contract, so a screen that stops listening really stops the network or sensor work behind it.
## Why the hooks exist A stream in Dart is meant to be **inert until someone listens** and to stop producing when its listener asks it to. An `async*` function gets both for free: its body does not run until `listen`, and it suspends at `yield` while the subscription is paused. A **`StreamController`** does neither by itself — it will happily accept `add` calls at any time and buffer them. The four lifecycle callbacks are how the producer learns what the consumer is doing. All four are optional named parameters of the default constructor, `StreamController({onListen, onPause, onResume, onCancel, sync = false})`, and they are also settable fields on the controller, which lets you create the controller first and wire the callbacks once you have a reference to it. ## When each one fires | Callback | Fires when | Typical job | |---|---|---| | `onListen` | The stream receives its listener (`listen`, `await for`, a `StreamBuilder` subscribing) | Start the source: subscribe to a socket, start a `Timer.periodic`, open a file | | `onPause` | The subscription goes from running to paused | Pause the upstream subscription or cancel the timer | | `onResume` | The subscription is resumed and its buffered events have drained | Resume the upstream subscription or restart the timer | | `onCancel` | The subscription ends | Cancel the upstream subscription, close the file, release native resources | A few details matter in an interview: - **Pause counting.** `pause()` can be called several times; `onPause` fires only on the first, and `onResume` only once every pause has been matched by a `resume()` *and* the events buffered during the pause have been delivered. - **Cancel on completion.** When the controller is closed and the done event reaches the listener, the subscription cancels itself *before* calling the listener's `onDone`. So `onCancel` runs on normal completion too, not just on an explicit `cancel()`. - **Async cleanup.** `onCancel` has type `FutureOr<void> Function()`. If it returns a `Future`, the listener's `subscription.cancel()` returns that future, and the done callback waits for it — useful when closing a socket takes time. - **Combined changes.** If a subscription and its pause state change at the same moment (for example, a listener cancels while paused), only `onListen` or `onCancel` fires, not `onPause`/`onResume`. That is why the dart.dev guidance says to wire **all four** when you care about pause. - **Errors.** An exception thrown from `onListen`, `onPause` or `onResume` is reported to the current zone as an uncaught error; on a default controller, one thrown from `onCancel` comes back through the future that `cancel()` returns. ## Broadcast controllers `StreamController.broadcast` accepts only `onListen`, `onCancel` and `sync`. `onListen` fires when the **first** listener arrives, `onCancel` when the **last** one leaves, and `onListen` fires again if someone listens after that. Pause callbacks are not supported: each broadcast subscription buffers for itself, and the controller's `isPaused` is always `false`. ## Chat-socket example For a chat client, the controller that republishes a socket's messages should: 1. in `onListen`, subscribe to the socket and forward each parsed message with `add`; 2. in `onPause`/`onResume`, call `pause()`/`resume()` on that socket subscription, so bytes stay in the operating system's buffers instead of in the controller; 3. in `onCancel`, cancel the socket subscription and return its future. If the forwarding is a plain one-to-one pipe, `addStream` or a `StreamTransformer` already forwards pause and cancel for you, which is often simpler than wiring the hooks by hand.
- Does onCancel run when the stream simply completes, with no explicit cancel()?Yes. When the done event is delivered, the subscription cancels itself first, which runs `onCancel`, and only then calls the listener's `onDone` — waiting for the future `onCancel` returned, if any. Cleanup placed in `onCancel` therefore covers both early cancellation and normal completion.
- Why does the controller's isPaused report true before anyone listens?For a single-subscription controller, `isPaused` means events would be buffered. With no listener yet, every `add` goes into the pending buffer, so the controller counts as paused. A producer that checks `isPaused` before doing expensive work therefore naturally waits for the first listener.
saying these in an interview costs you the question
- onCancel runs only when a listener explicitly calls cancel().
- onPause fires on every pause() call, even nested ones.
- A broadcast controller supports onPause and onResume like the default one.
- Starting the source in the constructor is fine because the controller buffers.
- onResume fires immediately on resume() even while buffered events remain.