Migration to Flutter Guide
Discover our battle-tested 21-step framework for a smooth and successful migration to Flutter!
Home
Glossary

FutureBuilder in Flutter

What is FutureBuilder in Flutter?

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.

Why does FutureBuilder matter in Flutter app development?

Flutter builds widgets frequently. Without a proper async pattern, this can easily lead to repeated API calls or inconsistent UI states.

FutureBuilder:

  • Keeps async logic declarative.
  • Avoids manual loading flags.
  • Reduces boilerplate for simple async screens.

How does FutureBuilder work?

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:

  • Whether the Future is still running.
  • Whether it completed with data.
  • Whether it failed with an error.

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.

Flutter FutureBuilder example (correct pattern)

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.

When to use FutureBuilder?

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.

When not to use FutureBuilder?

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 vs StreamBuilder

  • FutureBuilder → one-time result
  • StreamBuilder → multiple updates over time

If your data changes live, StreamBuilder is the correct choice.

Common mistakes to avoid

Common errors when using FutureBuilder can lead to bugs or unstable UI behavior:

  • Calling async methods directly in build().
  • Ignoring snapshot.hasError.
  • Accessing snapshot.data without checking hasData.
  • Using FutureBuilder for global app state.

Best practices

Following consistent practices helps ensure predictable and maintainable async UI handling:

  • Always create the Future outside build().
  • Handle loading, error, and empty states explicitly.
  • Keep FutureBuilder focused on a single responsibility.
  • Use state management for complex flows.

Learn more

Flutter architecture by LeanCode

Feature-Based Flutter App Architecture - LeanCode

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.

Flutter open source packages by LeanCode

6 Non-obvious Flutter and Dart Packages

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.