skip to content

Future & Stream Builders

FutureBuilder and StreamBuilder turn a Future or a Stream into widgets through an AsyncSnapshot. Interviewers ask why creating the future inside build() refires the request on every rebuild.

part ofFlutteroverview, primer and where to startread it →
on this pageshow

explore

questions

5

In Flutter, what do FutureBuilder and StreamBuilder do, and how should a builder render loading, error and data from an AsyncSnapshot?

level: juniorimportance: must knowfreq 70%

answer

  1. async source in, widget out
  2. builder(context, snapshot)
  3. hasError before hasData
  4. spinner as the fallback
  5. future: one result, stream: many

basics

~10 s

FutureBuilder and StreamBuilder subscribe to a Future or Stream and rebuild with an AsyncSnapshot describing its latest state. The builder checks snapshot.hasError, then hasData, and otherwise shows a loading indicator.

solid answer

~40 s

`FutureBuilder<T>` takes a `future`, `StreamBuilder<T>` takes a `stream`, and both take a `builder` of type `AsyncWidgetBuilder<T>`, `(BuildContext context, AsyncSnapshot<T> snapshot)`. They subscribe in their own `State`, and each time the future completes or the stream emits, they call `setState` and rebuild with a new snapshot. The builder turns that snapshot into UI: if `snapshot.hasError`, show an error with a retry; if `snapshot.hasData`, show the data, or an empty state when the data is an empty list; otherwise show a `CircularProgressIndicator`. A `FutureBuilder` delivers one result, a `StreamBuilder` keeps rebuilding for every event until the stream closes. The builder must stay free of side effects, because it runs on every rebuild.

code

dart · 35 lines
dart
import 'package:flutter/material.dart';

class ForecastView extends StatefulWidget {
  const ForecastView({super.key, required this.load});

  final Future<List<String>> Function() load;

  @override
  State<ForecastView> createState() => _ForecastViewState();
}

class _ForecastViewState extends State<ForecastView> {
  late Future<List<String>> _forecast = widget.load();

  @override
  Widget build(BuildContext context) {
    return FutureBuilder<List<String>>(
      future: _forecast,
      builder: (context, snapshot) {
        if (snapshot.hasError) {
          return TextButton(
            onPressed: () => setState(() => _forecast = widget.load()),
            child: const Text('Could not load forecast. Retry'),
          );
        }
        if (snapshot.hasData) {
          final days = snapshot.requireData;
          if (days.isEmpty) return const Text('No forecast available');
          return Column(children: [for (final d in days) Text(d)]);
        }
        return const Center(child: CircularProgressIndicator());
      },
    );
  }
}

go deeper

for a junior

Recall the builder signature, that the snapshot carries data, error and connection state, and the error, data, loading order of checks.

for a middle

Explain why hasData can be false on success, why errors are swallowed by default, and how an empty result differs from loading.

for a senior

Show you design every async screen with explicit loading, error with retry, empty and data states, and keep builders free of side effects.

for a principal

Judge when per-screen FutureBuilders are enough and when shared async state, caching and retry policy belong in a state library the team standardises on.

## What the two widgets are for Flutter's `build` methods are synchronous: they must return a widget right now. Data from the network, a database or a platform channel arrives later, as a Dart **`Future`** (one result) or **`Stream`** (a sequence of events). **`FutureBuilder`** and **`StreamBuilder`** bridge the gap. Each is a `StatefulWidget` that subscribes to its async source, stores the latest state of that source, and rebuilds its subtree whenever that state changes. | | `FutureBuilder<T>` | `StreamBuilder<T>` | |---|---|---| | Source parameter | `future` (`Future<T>?`, required but nullable) | `stream` (`Stream<T>?`, required but nullable) | | Results | one value or one error | any number of values and errors | | Rebuilds | when the future completes | on every event and when the stream closes | | Optional seed | `initialData` | `initialData` | | Builder type | `AsyncWidgetBuilder<T>` | `AsyncWidgetBuilder<T>` | ## The AsyncSnapshot The builder receives an immutable **`AsyncSnapshot<T>`** holding the most recent interaction with the source: - `connectionState`: one of `none`, `waiting`, `active`, `done`. - `data`: the latest value, or `initialData`, or `null`. - `error` and `stackTrace`: the latest error, if the last result was a failure. - `hasData` is simply `data != null`; `hasError` is `error != null`. - `requireData` returns the data, rethrows the error if there is one, and throws a `StateError` if there is neither. ## A builder that covers every state The usual order of checks, and why: 1. **Error first.** A failed future, or a stream error event, yields a snapshot with the error and no data; testing `hasError` first makes the failure branch explicit instead of letting it fall through to loading. 2. **Data next**, including the **empty** case: a successful result can still be an empty list, which deserves its own "no results" message rather than a blank screen. 3. **Otherwise loading**: a `CircularProgressIndicator` or skeleton. ```dart FutureBuilder<List<Forecast>>( future: _forecast, builder: (context, snapshot) { if (snapshot.hasError) { return ErrorPanel(onRetry: _retry); } if (snapshot.hasData) { final days = snapshot.requireData; if (days.isEmpty) return const Text('No forecast available'); return ForecastList(days: days); } return const Center(child: CircularProgressIndicator()); }, ) ``` One gap in the `hasData` check: a `Future<void>`, or any future that completes successfully with `null`, never has data. For those, test `snapshot.connectionState == ConnectionState.done` instead. ## Rules the builder must follow - **No side effects.** Do not start requests, navigate or show dialogs in `builder`; it may run many times. - **Do not create the future or stream in the same `build`** that constructs the builder widget; keep it in a `State` field so rebuilds do not restart it. - **Expect a subsequence of states.** The builder runs when the pipeline rebuilds, so it may never see some intermediate snapshots. - **Errors are not rethrown.** By default `FutureBuilder` swallows the future's error into the snapshot, so a builder that ignores `hasError` shows a spinner forever. ## Where these widgets fit They are the framework's built-in answer for one screen reading one async source. When several widgets share the same async data, or you need caching and retry policies, state libraries wrap the same ideas in their own types. In an interview, the concrete signals are the builder signature, the three-branch builder, and knowing that the source must be created outside `build`.

  • In Flutter, why does a FutureBuilder<void> never report hasData even after success?
    `hasData` is just `data != null`. A `Future<void>` completes with `null`, so the snapshot is `done` with no data and no error. Check `snapshot.connectionState == ConnectionState.done` together with `!snapshot.hasError` to detect success.
  • In Flutter, what happens if a FutureBuilder's builder ignores snapshot.hasError and the future fails?
    The error is stored in the snapshot and not rethrown by default, so the builder sees no data and falls through to its loading branch: the user watches a spinner forever. `FutureBuilder.debugRethrowError = true` makes debug builds surface the error while you investigate.

saying these in an interview costs you the question

  • FutureBuilder rethrows the future's error so it crashes visibly
  • hasData is true whenever the future completed successfully
  • It is fine to call the API inside the builder callback
  • StreamBuilder shows only the first event, like FutureBuilder
  • An empty list needs no separate state from loading
open as a page

In Flutter, why does a weather screen whose FutureBuilder calls fetchWeather() inside build() refetch on every rebuild, and how do you fix it?

level: middleimportance: must knowfreq 76%

basics

~20 s

Each build() call runs fetchWeather() again and passes FutureBuilder a new Future, so it resubscribes, shows waiting and fires another request. Create the future once in a State field or initState and pass that field.

open as a page

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%

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.

open as a page

In a Flutter weather screen built on FutureBuilder, how do you implement pull-to-refresh that keeps showing the old forecast instead of flashing a full-screen spinner?

level: seniorimportance: should knowfreq 32%

basics

~20 s

In RefreshIndicator.onRefresh, assign a new future in setState and return it. FutureBuilder keeps the previous result in snapshot.data while waiting, so the builder shows old data whenever hasData is true and spins only when there is none.

open as a page

In Flutter, when does a StreamBuilder subscribe to and cancel its stream, and what breaks when the stream is created inside build()?

level: seniorimportance: should knowfreq 30%

basics

~20 s

StreamBuilder 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.

open as a page