Skip to content

Futures & Streams in the UI

You cannot await inside buildbuild must be synchronous and may run many times. So Flutter gives you two widgets that subscribe to async sources and rebuild when they emit: FutureBuilder for a one-shot Future, and StreamBuilder for a Stream of values.

Both hand your builder an AsyncSnapshot, which describes the current state of the async source: are we still waiting, do we have data, or did it error?

FutureBuilder<User>(
future: _userFuture, // a Future created OUTSIDE build (see the trap)
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator(); // loading
}
if (snapshot.hasError) {
return Text('Failed: ${snapshot.error}'); // error
}
final user = snapshot.data!; // data is ready
return Text(user.name);
},
)

Read the snapshot in a fixed order: waiting → error → data. Handling all three (plus an empty state where relevant) is what separates a robust screen from one that flashes a null error for a moment.

StreamBuilder is the same shape for a Stream — it rebuilds on every emitted value, so it suits live data (a chat feed, a location stream, a Bloc state).

StreamBuilder<int>(
stream: _tickerStream,
initialData: 0,
builder: (context, snapshot) {
return Text('Count: ${snapshot.data}'); // rebuilds on each new value
},
)

This is the single most common FutureBuilder bug: creating the future inside build.

// ❌ WRONG — a new future is created on every rebuild,
// so the request re-fires and the loading spinner flickers forever.
Widget build(BuildContext context) {
return FutureBuilder(
future: fetchUser(42), // <-- recreated every build
builder: ...,
);
}
// ✅ RIGHT — create it once and store it (in initState for a StatefulWidget).
late final Future<User> _userFuture;
@override
void initState() {
super.initState();
_userFuture = fetchUser(42); // created once
}

Because build can run for many reasons (a parent rebuild, a theme change, a keyboard opening), any future created there is recreated each time — re-triggering the work and resetting the snapshot to waiting. Create it once, in initState or a state-management layer, and pass the same future in.

Why can't you just await inside build?
What does an AsyncSnapshot tell you?
What goes wrong when you create the Future inside build?
Where should a one-shot Future for a FutureBuilder be created?