FutureBuilder is a Flutter widget that builds UI based on the state of a Future. It is used to handle asynchronous operations such as API calls, database reads, or file access, and automatically rebuilds the UI when the operation completes.
Flutter builds widgets frequently. Without a proper async pattern, this can easily lead to repeated API calls or inconsistent UI states.
FutureBuilder:
It helps represent three core states: loading, success, and error. FutureBuilder listens to a Future<T> and exposes its state via an AsyncSnapshot.
The snapshot describes:
Future is still running.Your UI reacts to those states inside the builder callback.
Internally, it subscribes to the provided Future and registers a callback. When the Future completes (with data or error), the widget triggers a local setState, forcing a rebuild. The builder function is then called again with a new AsyncSnapshot containing the updated connection state and result, allowing the UI to transition purely based on the snapshot’s properties.
A safe and recommended approach is to create the Future once in initState():
class UserScreen extends StatefulWidget {
@override
State<UserScreen> createState() => _UserScreenState();
}
class _UserScreenState extends State<UserScreen> {
late Future<User> _userFuture;
@override
void initState() {
super.initState();
_userFuture = fetchUser();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<User>(
future: _userFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting)
{
return const CircularProgressIndicator();
}
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
}
if (!snapshot.hasData) {
return const Text('No data');
}
return Text(snapshot.data!.name);
},
);
}
}This prevents the Future from being recreated on every rebuild.
FutureBuilder is particularly useful when you need to integrate a single asynchronous operation directly into your widget tree. It shines for one-off tasks such as fetching initial data for a screen, reading a configuration file, or making a network request that occurs once when the screen loads. Using FutureBuilder allows you to declaratively handle loading, success, and error states without manually managing state variables or callbacks.
It works best when the result is static for the lifecycle of the widget and does not require continuous updates. Typical examples include displaying a user profile after a login API call, fetching a remote JSON file for configuration, or loading local database records when a screen first opens. FutureBuilder keeps the UI logic concise and readable, as the widget tree adapts automatically to the Future's state.
FutureBuilder is not ideal for data that changes over time or operations that need repeated execution. If your data stream updates frequently, such as real-time messages or live location updates, a StreamBuilder is more appropriate. Similarly, if your Future depends on variables that change on rebuild, recreating the Future inside the build() method can cause unnecessary network calls or repeated executions, leading to poor performance and flickering UI.
Avoid FutureBuilder when you require advanced caching, retries, or centralized state management, because embedding these behaviors inside a FutureBuilder can make the code harder to test and maintain. Instead, manage the Future outside the widget (for example, via a provider, BLoC, or Riverpod) and pass the result down to the widget tree. This ensures predictable builds, proper separation of concerns, and better control over asynchronous behavior.
FutureBuilder → one-time resultStreamBuilder → multiple updates over timeIf your data changes live, StreamBuilder is the correct choice.
Common errors when using FutureBuilder can lead to bugs or unstable UI behavior:
build().snapshot.hasError.snapshot.data without checking hasData.FutureBuilder for global app state.Following consistent practices helps ensure predictable and maintainable async UI handling:
Future outside build().FutureBuilder focused on a single responsibility.12 min • Jul 27, 2023
Read about the LeanCode approach to Flutter architecture. We highlight some of the design decisions that should be made when developing a feature in mobile apps. This includes our approach to dependency injection, state management, widget lifecycle, and data fetching.
8 min. • Nov 8, 2022
Almost every production-grade app depends on tens of packages. Using them allows for quick implementation of standard functionalities. Take a closer look at 6 useful but not-so-popular Dart and Flutter packages made and tested by LeanCode's devs.