Loading data
Apps often need to wait for data, then show a result—or an error—when it
arrives. Use FutureBuilder for one result and StreamBuilder for values that
keep arriving.
| The source produces… | Dart type | Build with |
|---|---|---|
| One eventual result | Future<T> | FutureBuilder<T> |
| Values over time | Stream<T> | StreamBuilder<T> |
| App-owned values changed by actions | A field or notifier | State management |
The builders do not fetch, parse, cache, or save anything. They listen to an
async source and pass its latest AsyncSnapshot<T> to your UI.
Load a result
Section titled “Load a result”Use FutureBuilder when a request produces one result. This example fetches
and decodes a real photo from Lorem Picsum, then
renders the loading, error, and success states. It uses two packages:
dart pub add http imagePress Load a photo to mount PhotoViewer. The preview waits for that press
so that opening this page sends no request to Lorem Picsum; the viewer itself
starts loading as soon as it builds.
Create the future once and keep it in state, as _photo does. A future created
in build would start a new request on every rebuild. widget.loadPhoto is
fetchPhoto with a new seed each time.
Check for an error before reading data, and keep side effects out of the
builder: it can run more than once for the same snapshot.
On browser surfaces and terminals with inline-image support, Image renders
true pixels. Other terminals fall back to character art from the same decoded
image.
Refresh and retry
Section titled “Refresh and retry”To load again, replace the future inside setState. The builder switches to
the new future and ignores a late result from the old one; the old request
itself is not cancelled.
Until the new future completes, the snapshot keeps the previous data and
error, with connectionState set to waiting. That is what keeps the last
photo on screen while Load another fetches the next one. It is also why the
viewer shows an error only when it is not loading: after a failed attempt,
Retry shows the loading state instead of repeating the old error, and the
disabled button prevents a second request while one is running.
Handle every state
Section titled “Handle every state”A snapshot’s connectionState says where the source is: none before there is
one, waiting, active (streams only), then done. data and error carry
the latest outcome. Most views need five states: nothing requested, loading,
failed, empty, and ready. Choose a state to connect a matching Future:
describe reads the connection state before the error, for the same reason as
the photo viewer, and AsyncStateCard draws its label and detail in a bordered
card. Choose Error, then Loading: the card shows the new request, not
the old failure.
Why the Error preview calls ..ignore()
A future that fails before anything listens reports an unhandled error, even
if a builder subscribes a moment later. The preview creates its failed future
ahead of the frame that renders it, so it marks the error as handled with
..ignore(). The builder still receives the error and renders the card. A real
request is usually handed to a builder or awaited right away; if you start one
early, handle its errors where you start it.
Keep heavy work off the UI isolate
Section titled “Keep heavy work off the UI isolate”A Fleury app renders, reads input, and handles signals on one Dart isolate.
Waiting on I/O costs nothing, but synchronous CPU work — decoding a large
image, parsing a big JSON document, sorting or diffing a large dataset — holds
all of it until the work finishes: no frames, no keystrokes, and not even
Ctrl+C, which arrives as input. Move that work to another isolate with
Isolate.run and give
the resulting future to a builder as usual:
import 'dart:isolate';import 'dart:typed_data';
import 'package:image/image.dart' as img;
Future<img.Image> decodePhotoInBackground(Uint8List bytes) => Isolate.run( () => img.decodeImage(bytes) ?? (throw const FormatException('Response was not an image')),);To use it in the photo example, replace the return statement at the end of
fetchPhoto, which calls img.decodeImage inline, with the background decode:
return decodePhotoInBackground(response.bodyBytes);Isolate.run works in terminal apps and under fleury serve, where the app
runs as a native process. A browser embed compiles to JavaScript, which has no
isolates, so keep work there small — which is why the live photo example above
decodes inline.
Stream updates
Section titled “Stream updates”Use StreamBuilder when values keep arriving. This demo treats a tiny star-map
transmission as five packets; each click emits a larger snapshot until the stream
closes:
Keep the stream in state so rebuilds do not subscribe again. initialData
supplies the data for the first frame, while the snapshot is still waiting;
the first packet makes it active, and closing the stream makes it done.
Restart creates a new transmission and stream. Like FutureBuilder, a
StreamBuilder that switches streams keeps showing the last data until the new
one emits. Keying the builder by the transmission starts it over from
initialData instead.
The builder cancels its subscription when it switches streams or leaves the
tree. It does not close the source: whoever creates the StreamController
closes it, as dispose and _restart do here. Dart’s
stream guide covers
transforming, listening to, and creating streams in more depth.
When builders stop being enough
Section titled “When builders stop being enough”Builders fit one subtree reflecting one async source. Move the work into an app-owned model or repository when widgets share it or you need caching, pagination, optimistic updates, coordinated sources, or offline policy. The UI can listen to that model with the tools in State management.