Flutter FutureBuilder Example: Async Done Right

Learn how the FutureBuilder widget makes asynchronous programming easier in Flutter.

This post was originally published on the Waldo Blog. The original version can be found here.


Introduction

Mobile apps can make use of data stored on the Internet or stored on the device. For example, consider an app that displays the latest movies currently available to watch. The list of movie titles, summary text, review comments, and related images come from a central server on the Internet.

Whether your app retrieves data stored on disk or on the Internet, it takes much longer to load than data that is in the temporary memory of the device. A computer processor can execute only one task at a time in a predefined order. If one of those tasks retrieves data from a slow channel, like the Internet, then all subsequent tasks must wait until the data comes back.

As an end user, you’d notice your computer freezing and being unresponsive during these moments. Such pauses in execution waste computer resources, since the processor is idle during these times. Moreover, it makes for a really bad user experience. Users of your app will likely complain that it’s slow and eventually they’ll become frustrated.

Luckily, we can solve this problem with a technique called asynchronous programming. Asynchronous programming enables one task to wait while other independent tasks continue to execute. Once the waiting task gets its data, it completes its execution. Furthermore, any task that needs the same data can continue its execution. The result is a better user experience and better utilization of your computer resources.

In this article, we’ll learn how to apply asynchronous programming to a Flutter app to make the experience better for users. Furthermore, we’ll explore a Flutter widget called FutureBuilder that helps us do asynchronous programming reliably with minimal code.

Async Example Without FutureBuilder

Let’s jump straight into an example. In this example, we want to display some information retrieved from the Internet. It takes a few seconds to deliver this data, so we want to show a progress indicator while waiting. When the data is available, we want to replace the progress indicator with our data from the Internet. The code below simulates a network data fetch and shows how to achieve the effect we want.  We set up variables and use setState to rebuild our widget when the data from the Internet arrives.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Async Example Without FutureBuilder';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: MyStatefulWidget(),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  final Future<String> _myNetworkData = Future<String>.delayed(
    const Duration(seconds: 4),
    () => 'This is what you have been waiting for',
  );
  bool isLoading = true;
  bool isDone = false;
  bool hasData = false;
  bool hasError = false;

  String myData = '';

  @override
  void initState() {
    getData();
    super.initState();
  }

  void getData() {
    _myNetworkData
        .then((value) => setState(() {
              myData = value;
              isLoading = false;
              isDone = true;
              hasData = true;
              hasError = false;
            }))
        .catchError((error) {
      myData = error.toString();
      isLoading = false;
      isDone = true;
      hasData = false;
      hasError = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    List<Widget> children;
    if (isDone && hasData) {
      children = <Widget>[
        const Icon(
          Icons.thumb_up,
          color: Colors.purple,
          size: 100,
        ),
        Padding(
          padding: const EdgeInsets.only(top: 30),
          child: Text(
            'Done: $myData',
            style: TextStyle(fontSize: 20),
          ),
        )
      ];
    } else if (isDone && hasError) {
      children = <Widget>[
        const Icon(
          Icons.error,
          color: Colors.red,
          size: 100,
        ),
        Padding(
          padding: const EdgeInsets.only(top: 30),
          child: Text(
            'Error: $myData',
            style: TextStyle(fontSize: 20),
          ),
        )
      ];
    } else {
      children = const <Widget>[
        SizedBox(
          child: CircularProgressIndicator(
            color: Colors.blue,
          ),
          width: 80,
          height: 80,
        ),
        Padding(
          padding: EdgeInsets.only(top: 30),
          child: Text(
            'Retrieving Data',
            style: TextStyle(fontSize: 20),
          ),
        )
      ];
    }

    return Scaffold(
      appBar: AppBar(
        title: Text('Async Without FutureBuilder'),
      ),
      body: Center(
          child: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          crossAxisAlignment: CrossAxisAlignment.center,
          children: children,
        ),
      )),
    );
  }
}

Notice how much work was involved in tying all the pieces together to make this work. We needed to define state variables to represent the current state of the asynchronous operation. Then, we had to set completion handlers on the asynchronous operation to update those variables, while triggering setState() to rebuild the widget. Finally, we needed to link those state variables within our widget’s build function. In the next section, we’ll learn how FutureBuilder makes this much easier for us.

Async Example Using Flutter FutureBuilder

The example described above is such a common use case that the creators of Flutter have provided us with an easier solution. The Flutter framework provides a widget named FutureBuilder that allows us to do asynchronous programming in a much cleaner way, with widgets. See below for how to build the same app using FutureBuilder. Note how much cleaner and less error prone this implementation is. In the next section, we’ll explore in detail how FutureBuilder works.

import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({Key? key}) : super(key: key);

  static const String _title = 'Async Example using FutureBuilder';

  @override
  Widget build(BuildContext context) {
    return const MaterialApp(
      title: _title,
      home: MyStatefulWidget(),
    );
  }
}

class MyStatefulWidget extends StatefulWidget {
  const MyStatefulWidget({Key? key}) : super(key: key);

  @override
  State<MyStatefulWidget> createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  final Future<String> _myNetworkData = Future<String>.delayed(
    const Duration(seconds: 4),
    () => 'This is what you have been waiting for',
  );

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Async With FutureBuilder'),
      ),
      body: Center(
        child: FutureBuilder<String>(
          future: _myNetworkData,
          builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
            List<Widget> children;
            if (snapshot.connectionState == ConnectionState.done &&
                snapshot.hasData) {
              children = <Widget>[
                const Icon(
                  Icons.thumb_up,
                  color: Colors.purple,
                  size: 100,
                ),
                Padding(
                  padding: const EdgeInsets.only(top: 30),
                  child: Text(
                    'Done: ${snapshot.data}',
                    style: TextStyle(fontSize: 20),
                  ),
                )
              ];
            } else if (snapshot.connectionState == ConnectionState.done &&
                snapshot.hasError) {
              children = <Widget>[
                const Icon(
                  Icons.error,
                  color: Colors.red,
                  size: 100,
                ),
                Padding(
                  padding: const EdgeInsets.only(top: 30),
                  child: Text(
                    'Error: ${snapshot.error}',
                    style: TextStyle(fontSize: 20),
                  ),
                )
              ];
            } else {
              children = const <Widget>[
                SizedBox(
                  child: CircularProgressIndicator(
                    color: Colors.blue,
                  ),
                  width: 80,
                  height: 80,
                ),
                Padding(
                  padding: EdgeInsets.only(top: 30),
                  child: Text(
                    'Retrieving Data',
                    style: TextStyle(fontSize: 20),
                  ),
                )
              ];
            }
            return Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                crossAxisAlignment: CrossAxisAlignment.center,
                children: children,
              ),
            );
          },
        ),
      ),
    );
  }
}

How FutureBuilder Works

In the section above we saw an example application of FutureBuilder. Let’s now learn more about what goes into making FutureBuilder work. Below are the three properties assigned in the FutureBuilder constructor to configure its behavior.

  • The future property assigns the Future value that will be delivered asynchronously, such as a list of movies from the Internet. Later in this section, we’ll explain in more detail what a Future is.
  • The builder property is of type AsyncWidgetBuilder, which is a function that takes in a BuildContext and an AsyncSnapshot and returns a Widget. Later, we’ll talk more about the AsyncSnapshot and how to use it in the builder function body.
  • The initialData property provides a snapshot of placeholder data that can be rendered by the builder while waiting for the real data to be delivered via the Future object.

What Is a Future

Understanding how a FutureBuilder works requires a good understanding of what a Future is and how it works. Let’s use an analogy to explain. Imagine ordering food from a fast food restaurant. The cashier takes your order and gives you a receipt. It takes a few minutes for your food to be prepared and then your receipt number is called. You then take your food and continue with your day. A Future is like the receipt from the restaurant. They cannot give you your food immediately, so they give you a receipt that describes the food you can expect to receive in the future. You wait until your food is ready and then consume it once it arrives. In the same way, a Future is a promise of data to come at some point in the future.

What Is an AsyncSnapshot

An AsyncSnapshot is the container that will hold the data once it is delivered. Extending the fast food restaurant analogy from the previous section, when you place an order, the cashier places a tray to the side to hold your food once it arrives. You can think of an AsyncSnapshot as that tray. Instead of food, we have data. The data can be retrieved via the data property of the AsyncSnapshot. The AsyncSnapshot also has other useful properties, such as hasData, hasError, and connectionState, which we’ll discuss in the next section.

What Is a ConnectionState

The connectionState property of an AsyncSnapshot object provides useful information about the data that we’re waiting for. It can be one of the following:

  • ConnectionState.none, which means that the asynchronous task to retrieve the data has not begun;
  • ConnectionState.waiting, which means that the asynchronous task is in progress; or
  • ConnectionState.done, which means that the task has completed either successfully with data or with an error.

Within the AsyncWidgetBuilder function body, we can check the connectionState property of the AsyncSnapshot to decide what to display in the widget. For example, if it is ConnectionState.none or ConnectionState.waiting, then we can display a progress indicator. If it is ConnectionState.done, then we can display the data, given that the hasError property is false and the hasData property is true. If something went wrong and hasError is true, then we can display an error message.

In this section, we took a deep dive into the FutureBuilder widget to show all that it offers to help us apply asynchronous programming for building widgets that depend on data not immediately available. We learned about Future, AsyncSnapshot, and ConnectionState objects, and how they help us create robust asynchronous code.

Conclusion

In this article, you learned about asynchronous programming and how it helps to keep apps active while waiting for data to arrive from the Internet or from local disk storage. We discussed how asynchronous programming is essential for keeping apps responsive and users engaged.

We discovered how Flutter’s FutureBuilder widget can save us effort and lines of code when we need to construct a widget based on data sourced from the Internet. Furthermore, we learned how to fully implement this to account for all cases when the data is successfully retrieved and also when an error occurs.

With this knowledge, you are now equipped to make use of the FutureBuilder widget to build apps that can retrieve data from the Internet and automatically display the contents once the data is retrieved.

To learn more about Flutter development, visit the Waldo blog. This is an excellent resource for learning about mobile development, design and testing.

Subscribe to Daliso Zuze's Blog

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe