Futures & Streams in the UI
The bridge from async to widgets
Section titled “The bridge from async to widgets”You cannot await inside build — build 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
Section titled “FutureBuilder”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
Section titled “StreamBuilder”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 },)The create-in-build trap
Section titled “The create-in-build trap”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;@overridevoid 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.