In Flutter, when does a StreamBuilder subscribe to and cancel its stream, and what breaks when the stream is created inside build()?
answer
- listen in initState
- cancel in dispose
- new stream means cancel and relisten
- single-subscription listens once
- keep the stream in State
basics
~20 sStreamBuilder listens in initState, cancels in dispose, and when given a different stream cancels the old subscription and listens to the new one. A stream created in build() is replaced on each rebuild, restarting the source and resetting to waiting.
solid answer
~50 s`StreamBuilder`'s state calls `stream.listen` in `initState` and `cancel()` in `dispose`, so the widget owns its subscription and cleans it up when removed. In `didUpdateWidget`, if the new `stream` is not `==` to the old one, it cancels the old subscription, moves the snapshot to `none` then `waiting`, and listens to the new one. If `build` creates the stream, for example `stream: db.watchForecast(city)` or a `.map(...)` on a source, each rebuild produces a new stream object: the query restarts, events emitted in between are missed, and the UI flickers to `waiting`. With a single-subscription stream kept in a field, the opposite bug appears: a second `StreamBuilder`, or the same one re-created after leaving the tree, calling `listen` again throws a `StateError`. Create or obtain the stream once in `State`, or expose a broadcast stream from its owner.
code
dart · 29 linesclass _ForecastFeedState extends State<ForecastFeed> {
late Stream<List<String>> _days;
@override
void initState() {
super.initState();
_days = widget.watch(widget.city);
}
@override
void didUpdateWidget(ForecastFeed oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.city != widget.city) {
_days = widget.watch(widget.city);
}
}
@override
Widget build(BuildContext context) {
return StreamBuilder<List<String>>(
stream: _days,
builder: (context, snapshot) {
if (snapshot.hasError) return const Text('Feed error');
if (!snapshot.hasData) return const CircularProgressIndicator();
return Column(children: [for (final d in snapshot.requireData) Text(d)]);
},
);
}
}go deeper
Remember that StreamBuilder listens and cancels for you, and that the stream must be created once, not inside build.
Explain didUpdateWidget's cancel and relisten when the stream object changes, and why a .map in build counts as a new stream.
Diagnose restarted queries, missed events and StateErrors on re-created builders, and choose between per-listener, broadcast and cached-state designs.
Set where long-lived streams are owned and shared in the app so widgets subscribe to stable sources instead of creating their own.
## The subscription lifecycle `StreamBuilder` extends `StreamBuilderBase`, whose `State` manages a single `StreamSubscription`: | Moment | What `StreamBuilder` does | |---|---| | `initState` | sets the initial snapshot, calls `stream.listen(...)` if the stream is non-null, moves to `waiting` | | each data event | `setState` with an `active` snapshot carrying the value | | each error event | `setState` with an `active` snapshot carrying the error | | stream closes | `setState` to `done` | | `didUpdateWidget` with a different stream | cancels the old subscription, snapshot to `none`, listens to the new stream, `waiting` | | `dispose` | cancels the subscription | Because it cancels in `dispose`, a `StreamBuilder` does not leak its own subscription. What it cannot protect you from is the stream object you give it. ## Creating the stream in build Consider a forecast screen that watches a local database table: ```dart @override Widget build(BuildContext context) { return StreamBuilder<List<Forecast>>( stream: db.watchForecast(widget.city).map(sortByDay), // new stream every build builder: (context, snapshot) => ForecastList(snapshot: snapshot), ); } ``` Each `.map` call returns a **new `Stream` object**, and a `watch...()` method typically does too. On every rebuild, `didUpdateWidget` sees a different stream and: 1. cancels the old subscription, which may tear down a database query or a platform listener; 2. listens to the new stream, which may start the query again; 3. drops the snapshot back to `waiting` with the old data, so the UI can flicker; 4. loses any event emitted between cancel and the new subscription's first event, for sources that do not replay. The fix mirrors the `FutureBuilder` rule: obtain the stream in `initState`, re-create it in `didUpdateWidget` when an input such as `widget.city` changes, and pass the stored field. ## The single-subscription trap Many Dart streams, including the stream of a default `StreamController`, are **single-subscription**: they allow one `listen` for their whole life. Storing such a stream in a field fixes the rebuild problem, but two situations call `listen` again: - Two `StreamBuilder`s given the same single-subscription stream. - A `StreamBuilder` that leaves the tree and comes back, for instance in a tab that is not kept alive, creating a new `State` that listens to the stored stream a second time. The second `listen` throws a `StateError`. Options: - Give each listener its own stream by calling the owner's `watch...()` method per subscriber, in that subscriber's `State`. - Have the owner expose a **broadcast** stream (for example from a broadcast `StreamController`), accepting that late listeners miss earlier events. - Keep the data in a notifier or state library that caches the latest value, so late listeners still get it. The details of single-subscription versus broadcast streams belong to Dart's stream semantics; what matters here is recognising the error when a `StreamBuilder` is re-created. ## Checklist - Never build a stream, or chain `.map`/`.where` onto one, inside `build`. - Re-create the stream only when its inputs change, in `didUpdateWidget` or `didChangeDependencies`. - Let `StreamBuilder` own the subscription; do not also `listen` manually to the same stream. - For streams shared across widgets, decide explicitly between per-listener streams, broadcast streams and a cached state holder. - Remember that `initialData` only covers the first frames; it does not replay events a new subscription missed.
- In Flutter, do you need to cancel a StreamBuilder's subscription yourself?No. `StreamBuilder` cancels its subscription in its own `dispose` and when it switches to a different stream. You still own the stream's source, such as a `StreamController` you created, and must close that in your own `State.dispose`.
- In Flutter, why can a StreamBuilder inside a tab throw a StateError when you return to the tab?If the tab's subtree was disposed, returning creates a new `StreamBuilder` state that calls `listen` on the stored stream again. A single-subscription stream allows only one listen, so it throws. Create the stream per subscriber, use a broadcast stream, or keep the tab alive.
saying these in an interview costs you the question
- StreamBuilder never cancels, so every one leaks a subscription
- Chaining .map onto a stream inside build is harmless
- Any stream can be listened to by several StreamBuilders
- initialData replays events missed while resubscribing
- StreamBuilder resubscribes on every rebuild even with the same stream