skip to content

In Flutter, which ConnectionState values can an AsyncSnapshot pass through for a FutureBuilder versus a StreamBuilder, and what does initialData change?

level: middleimportance: should knowfreq 48%

answer

  1. none, waiting, active, done
  2. null source means none
  3. active is the stream's state
  4. error on a stream stays active
  5. initialData seeds data, not state

basics

~20 s

ConnectionState is none, waiting, active or done. A FutureBuilder goes none or waiting to done; a StreamBuilder goes waiting, then active per event, then done on close. initialData fills snapshot.data before the first result, without changing the state.

solid answer

~40 s

`ConnectionState` has four values. `none` means no source: the `future` or `stream` is null. `waiting` means subscribed but nothing received yet. `active` is used only by `StreamBuilder`, once events arrive and until the stream closes. `done` means the future completed or the stream closed. A `FutureBuilder` snapshot therefore moves `waiting` to `done` (the builder may skip `waiting`), while a `StreamBuilder` moves `waiting` to `active` for each event and to `done` on close. A stream error produces an `active` snapshot with the error and no data; a later value replaces it. `initialData` makes `data` non-null from the first build, so `hasData` is true while still `waiting`: code that branches on `hasData` alone never shows a spinner.

code

dart · 17 lines
dart
StreamBuilder<double>(
  stream: temperatureStream,
  initialData: 18.0,
  builder: (context, snapshot) {
    final state = snapshot.connectionState;
    if (snapshot.hasError) {
      return Text('Sensor error: ${snapshot.error}');
    }
    return Row(
      children: [
        Text('${snapshot.requireData.toStringAsFixed(1)} C'),
        if (state == ConnectionState.waiting) const Text(' (connecting)'),
        if (state == ConnectionState.done) const Text(' (sensor closed)'),
      ],
    );
  },
)

go deeper

for a junior

Recall the four ConnectionState names and that none means the future or stream is null.

for a middle

Explain the future and stream sequences, that active belongs to streams, and how initialData makes hasData true while still waiting.

for a senior

Use connectionState and data together to show stale-while-refreshing UI, and handle stream errors as recoverable active snapshots rather than terminal failures.

for a principal

Set a team convention for async screen states so loading, stale, error and ended look consistent across features, whatever widget or library produces them.

## The four states `ConnectionState` is an enum in the widgets library describing how a builder is connected to its asynchronous source: | Value | Meaning | Typical data | |---|---|---| | `none` | not connected: the source is `null` | `initialData`, or previous result | | `waiting` | subscribed, nothing received yet | `initialData`, or previous result | | `active` | a stream has produced events and is still open | latest event | | `done` | the future completed, or the stream closed | final value, or error | `connectionState` answers *where in its lifecycle* the source is; `data` and `error` answer *what it last produced*. They are independent, which is why checking one without the other misleads. ## FutureBuilder's sequence A `FutureBuilder` never uses `active`: 1. On `initState`, the snapshot starts as `none` (with `initialData` if given). 2. If `future` is non-null, it subscribes and moves to `waiting`. 3. When the future completes, `setState` sets `done` with data, or `done` with an error. The builder is called when the pipeline rebuilds, so it may see both snapshots or only `done`. A new future that has **already completed** still produces one frame of `waiting`, because Dart has no way to tell synchronously that a `Future` is complete. Switching to a new future keeps the old data: the snapshot passes through `none` and `waiting` still carrying the previous result. ## StreamBuilder's sequence 1. Starts at `none`, or `none` with `initialData`. 2. After `listen`, moves to `waiting`, keeping any `initialData`. 3. Each data event produces `active` with that value. 4. Each **error event** produces `active` with the error and **no data**. The stream keeps going; the next data event replaces the error-only snapshot. 5. When the stream closes, the snapshot moves to `done`, keeping its last data or error. If you give it a different stream, it cancels the old subscription, moves to `none` with the old data, then `waiting` on the new stream. ## What initialData changes **`initialData`** seeds `snapshot.data` so the first frames have something to show: a cached forecast, a zero count, an empty list. It changes **data, not state**: - `hasData` is `true` from the very first build, while `connectionState` is still `waiting`. - A builder that branches only on `hasData` therefore never shows a loading indicator. - For a `FutureBuilder`, if the future fails, `data` becomes `null` regardless of `initialData`, and the error is in `snapshot.error`. That is often exactly what you want (show cached data immediately and replace it quietly), but if the design needs a "refreshing" hint, combine the checks: data present *and* `connectionState == ConnectionState.waiting`. ## Branching correctly - Use `hasError` and `hasData` to decide **what content** to show. - Use `connectionState` to decide **whether work is in flight** (a small progress bar over stale data) or whether a stream has **ended** (`done`). - Treat `ConnectionState.none` as a configuration state: the source is null, for example before the user has picked a city. - A `Future<void>` completes with `null`, so detect its success with `done` and no error. ## Summary for an interview - `active` belongs to streams; futures go straight from `waiting` to `done`. - Stream errors are `active` snapshots without data, not a terminal state. - `initialData` makes `hasData` true during `waiting`. - Snapshots may be skipped; never count on seeing every one.

  • In Flutter, does a StreamBuilder stop listening after the stream emits an error?
    No. The error event produces an `active` snapshot with the error and no data, and the subscription stays open. If the stream emits another value, the next snapshot is `active` with that value and no error. Only closing the stream moves it to `done`.
  • In Flutter, why can a FutureBuilder given an already-completed future still show one frame of waiting?
    `FutureBuilder` subscribes with `then`, which reports the result asynchronously for ordinary futures, and there is no synchronous way to ask a `Future` whether it has completed. So the first build after subscribing sees `waiting`, and the result arrives on the next rebuild.

saying these in an interview costs you the question

  • FutureBuilder reports ConnectionState.active while the request runs
  • A stream error moves StreamBuilder to done and ends the subscription
  • initialData changes connectionState to done on the first build
  • ConnectionState.none means the request is still loading
  • Every snapshot state is guaranteed to reach the builder