FutureBuilder, async, await in Flutter, practical example

Video thumbnail

Today we are going to discuss a misunderstood widget such as the FutureBuilder; which at first glance may seem abstract and difficult to use, but it has a very particular use that can help us successfully complete a development that involves an asynchronous process with which, in the end, we want to draw a widget.

Surely if you are starting in Flutter, the FutureBuilder seems like one of those “mysterious” and difficult-to-use widgets that everyone uses but no one fully explains. And I'll be honest: the first time I saw it, I thought exactly what you probably thought: “Now what do I do with this?”

This guide will help you understand how to use the FutureBuilder widget correctly; let's remember that we agreed that we know how to create lists with the ListView in Flutter.

The FutureBuilder is not complicated: you just need to master its execution logic and know what to expect from the famous snapshot.

What FutureBuilder is and what it is for

The FutureBuilder is a widget that allows you to build UI based on the result of a Future, that is, an asynchronous process that will finish “later.” In Flutter, you cannot block the UI while waiting for data, so you need an elegant way to:

  • Display something while loading.
  • Show the result when you have it.
  • Handle errors if something fails.

That is exactly what FutureBuilder solves.

Futures and asynchronous UI

A Future<T> is a promise: “I will return a value of type T to you… but not now.” If you come from JavaScript or Python, these are the famous async and await. Every time we make an HTTP request, it is an asynchronous process, and in mobile apps, in 99.9% of cases, we use a FutureBuilder for some operation with the network, that is, with the Internet.

It could be an API call, a read operation, or any work that is not immediate.

When you build UI that depends on a future, you need to react to its states. This is where the FutureBuilder comes in.

How FutureBuilder works (clear and direct explanation)

Essential parameters: future and builder

These are the two governing parameters:

  • future: → the asynchronous function you are going to execute
  • builder: → a callback that is automatically rebuilt when the future finishes

The snapshot and its states

The snapshot has critical information:

  • snapshot.hasData, if there is data.
  • snapshot.hasError, if an error occurred.
  • snapshot.connectionState, connection state.
    • ConnectionState.waiting → waiting for response
    • ConnectionState.done → future resolved
    • ConnectionState.active → very rarely used with FutureBuilder
    • ConnectionState.none → no future was executed
  • snapshot.data, the data.

And here is an important point I learned in a real project:
if your future throws an exception, the FutureBuilder never reaches the “done” state, so don't blindly trust hasData.

Real example: FutureBuilder with an API

The FutureBuilder widget 

For demonstration purposes, the asynchronous process, or what is the same, that function that returns a Future, which in practice is a function that performs at least one asynchronous process, connects to the Internet via a URL and obtains data:

static Future<List<dynamic>> getComments(int classId) async {
   var url = Uri.https(baseUrlAPI, "<URL>");

   try {
     final response = await http.get(url);
     if (response.statusCode == 200) {
        return jsonMap['comments']);
     }
   } on Exception {}
   return [];
 }

Building the UI in the builder

Now, the future is as easy to use as indicating the previously created asynchronous method in the future parameter. Then, in the builder parameter, which is a callback, is where we build the widget that needs the input data provided by the future; in our example, it is the list of data:

Widget _listComments() {

   return FutureBuilder(
     future: AcademyHelper.getComments(5),
     builder: (_, snapshot) {

       if (snapshot.hasData && snapshot.data != null) {
         return ListView.builder(
             shrinkWrap: true,
             itemCount: snapshot.data?.length, // comments
             itemBuilder: (_, int position) =>
                 Text(snapshot.data?[position]['comment']));
       } else {
         return const CircularProgressIndicator();
       }
     },
   );
 }

As you can see, the data is not returned to us directly, but rather we have an object called snapshot, which is nothing more than a wrapper for our data; the reason for this is that this object gives us the status of the request, since, being an asynchronous process, we will want to draw a widget BEFORE loading the data, which is then replaced by the widget built from the data.

The snapshot has parameters like hasData that specify whether the future has been resolved; it is very important that an exception does NOT occur from the future since, otherwise, the future would never be resolved.

Typical errors I have seen (and how to avoid them)

  • The future throws an exception and is never resolved → always catch errors.
  • Making the call inside the builder → very bad, causes multiple requests.
  • Returning null instead of empty lists → generates silent errors.
  • Abusing FutureBuilder on the same screen → preferably combine it with controllers or Providers.

Best practices, use try catch DAMN IT!

The most common error is that the future is never resolved, I always do:

try {} catch (_) {
 return [];
}

When NOT to use FutureBuilder

  • When you need multiple real-time updates → use StreamBuilder.
  • When data is used in multiple widgets → use global state (Provider, Riverpod, Bloc).
  • When the screen has multiple dependent sections → creating a FutureBuilder for each can be unnecessary.

Quick FAQs about FutureBuilder

  • How do I handle errors within FutureBuilder?
    • Catch exceptions in the future and handle hasError.
  • Why doesn't FutureBuilder run again?
    • Because a future only runs once. Use a method that generates a new future or encapsulate it in a button/action.
  • Can I use multiple FutureBuilders on one screen?
    • Yes, but don't overdo it. The screen can become slow.
  • Is it bad to nest FutureBuilders?
    • Almost always yes. Chain futures or use global state.

Conclusion

In the end, the FutureBuilder is particularly useful when we cannot use an asynchronous process directly in the widget tree, and we cannot use a setState to redraw the widget, because the FutureBuilder rebuilds itself automatically upon obtaining the data; it is as if it were an automated statefulwidget that is built automatically upon obtaining the data.

The FutureBuilder is not complicated: it is predictable and very useful when you have an asynchronous process that finishes a single time.

After using it repeatedly to load data from APIs, I understood that its real power lies in freeing you from manual state: it rebuilds the UI for you when the data arrives.

If you learn to read the snapshot, avoid executing futures multiple times, and handle errors carefully, it becomes one of the most reliable widgets in the Flutter ecosystem.

The next step is to learn how to use routes and navigation in Flutter.

I agree to receive announcements of interest about this Blog.

Today we are going to discuss a misunderstood widget such as FutureBuilder; which at first glance may seem abstract and difficult to use, but it has a very particular use that can help us successfully complete a development that involves an asynchronous process with which, in the end, we want to draw a widget.

| 👤 Andrés Cruz

🇪🇸 En español