skip to content

Built-In Reactivity

The framework's own state tools: setState scope, ChangeNotifier and ValueNotifier listenables, and FutureBuilder and StreamBuilder. Interviewers ask when these stop being enough for an app.

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

explore

questions

19

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, what distinguishes ephemeral state from app state, and where should a password field's show/hide toggle live?

level: juniorimportance: must knowfreq 68%

basics

~20 s

Ephemeral state belongs to one widget — a visibility toggle, the selected tab — and lives in its State behind setState. App state is shared across screens or kept between sessions, like the signed-in user. The password toggle is ephemeral.

open as a page

In Flutter, what is a ChangeNotifier, and how does a ListenableBuilder rebuild when that notifier calls notifyListeners()?

level: juniorimportance: must knowfreq 62%

basics

~10 s

ChangeNotifier is a foundation class that holds mutable state outside widgets and calls its registered listeners when notifyListeners() runs. ListenableBuilder subscribes to it and calls setState internally, so only the builder's subtree rebuilds.

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, how do Provider, Riverpod and BLoC differ on state scope, testability, boilerplate and async handling?

level: middleimportance: must knowfreq 72%

basics

~20 s

Provider scopes objects to the widget tree and looks them up by type through BuildContext. Riverpod keeps state in a ProviderContainer, readable without BuildContext and overridable in tests. BLoC turns events into a stream of states: most boilerplate, most explicit.

open as a page

In Flutter, how do you lift state to a common ancestor, and how do child widgets read and change it through constructor values and callbacks?

level: middleimportance: must knowfreq 60%

basics

~20 s

Move the value into the State of the nearest widget above everyone who reads it, pass it down as constructor arguments, and pass callbacks such as ValueChanged<bool> down so children report changes; the owner updates the value with setState.

open as a page

In Flutter, who should call dispose() on a ChangeNotifier that a ListenableBuilder listens to, and what goes wrong when the wrong object does?

level: middleimportance: must knowfreq 56%

basics

~20 s

The object that created the notifier disposes it, typically a State that created it in a field or initState and disposes it in its own dispose(). ListenableBuilder only removes its listener; disposing a notifier you were handed breaks every other listener.

open as a page

In Flutter's official documentation, is one state management library prescribed, and what does the app-architecture case study actually use?

level: juniorimportance: should knowfreq 42%

basics

~20 s

No. The docs present setState, the built-in notifier and InheritedWidget tools, and community packages, and say the best choice depends on the app and team. The architecture case study uses ChangeNotifier view models with ListenableBuilder, and package:provider for dependency injection.

open as a page

In Flutter, which part of the tree rebuilds when a State calls setState, and why keep ephemeral state in the smallest widget that needs it?

level: juniorimportance: should knowfreq 52%

basics

~20 s

setState reruns build for that one State, and the widgets it returns update down its subtree; ancestors and siblings are untouched. Keeping state in the smallest widget that needs it keeps each rebuild small, which Flutter's docs call pushing state to the leaves.

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 Flutter, how does ValueNotifier differ from ChangeNotifier, and why can a ValueNotifier<List<String>> fail to rebuild its ValueListenableBuilder?

level: middleimportance: should knowfreq 50%

basics

~20 s

ValueNotifier is a ChangeNotifier holding one value; assigning value notifies only when the new value is not == to the old. Mutating a list in place changes nothing the setter can see, so ValueListenableBuilder never rebuilds.

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

In Flutter, what trade-offs does GetX make compared with Riverpod or BLoC, and when would you advise a team against adopting it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

GetX bundles reactive state, a global dependency registry and navigation behind context-free calls, so features ship fast with little code. The price is hidden global coupling, test setup against a shared registry, and an all-in-one dependency that is costly to remove.

open as a page

In a growing Flutter notes app, how do you decide where each piece of state lives, and when does passing values and callbacks down stop scaling?

level: seniorimportance: should knowfreq 45%

basics

~20 s

Put each value in the lowest widget above all its readers and writers that lives as long as the value must. Passing values and callbacks down stops scaling when middle widgets only forward them, or when several routes need the same live data.

open as a page

In a Flutter sheet-music app, a score download's progress callback calls setState on the whole screen many times a second and the page janks; how would you restructure it with notifiers and builders?

level: seniorimportance: should knowfreq 34%

basics

~20 s

Keep the progress in a ValueNotifier<double> owned by the download controller, and wrap only the LinearProgressIndicator in a ValueListenableBuilder. Combine progress with status via Listenable.merge, built once, so each tick rebuilds a few widgets instead of the screen.

open as a page

A five-person team is starting a Flutter clinic-appointment app; how would you choose its state management library and make the choice hold as the app grows?

level: principalimportance: should knowfreq 45%

basics

~20 s

Inventory the app's state, weight scope, testability, boilerplate, async needs and team skills, then spike one real flow, such as booking a slot, in the two leading candidates. Record the decision with its reasons, and enforce conventions so five people use it one way.

open as a page

In Flutter, where do flutter_redux and signals-style packages sit on scope, boilerplate and rebuild granularity compared with BLoC and Riverpod?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

flutter_redux keeps all app state in one Store changed only by actions through pure reducers: maximum predictability and ceremony. Signals-style packages sit at the opposite end: small reactive values that track who reads them, little boilerplate, very fine-grained rebuilds.

open as a page

In Flutter, is ChangeNotifier.notifyListeners() synchronous, and what happens when you call it while the widget tree is being built?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Yes. notifyListeners() calls every listener immediately, in registration order. Called from a build method, the listening builders try to mark themselves dirty mid-build, and unless they are descendants of the widget building, debug mode throws 'setState() or markNeedsBuild() called during build.'

open as a page