Async in Flutter - FutureBuilder

6 min read, 28 July, 2018

After reading the article Async in Flutter - Futures, you should have a basic idea of how to use Futures with its API directly or combined with async and await. In Flutter apps, when the computation represented by a Future is completed, the UI may need to update based on the result. For example, if we use http to retrieve some data from the back-end server, when the Future completes successfully, we need to display the result to the user; when the Future completes with an error, we need to display notifications to the user; when the request is still in progress, we may want to show a loading spinner to indicate that. This usually means we need to have different UIs for the three possible states that a Future may be in.

  • pending - Display a loading spinner.
  • completed with value - Display the result.
  • completed with error - Display an error notification.

Flutter has a built-in stateful widget FutureBuilder that builds itself based on the latest snapshot of interaction with a Future.

FutureBuilder Basics

Let's start from the definition of class FutureBuilder. Below is the constructor of FutureBuilder.

FutureBuilder({Key key, Future<T> future, T initialData,  AsyncWidgetBuilder<T> builder })

In the constructor,

  • future is the Future object represents the asynchronous computation this builder is currently connected.
  • initialData is the initial snapshot of data before a non-null future has completed.
  • builder of type AsyncWidgetBuilder is a function to build the widgets based on asynchronous interaction.

The builder function accepts two parameters BuildContext context and AsyncSnapshot<T> snapshot, and returns a Widget. AsyncSnapshot contains information of asynchronous computation. It has following properties:

  • connectionState - Value of the enum ConnectionState that represents state of connection to an asynchronous computation. ConnectionState has four values: none, waiting, active and done.
  • data - The latest data received by the asynchronous computation.
  • error - The latest error object received by the asynchronous computation.

AsyncSnapshot also has properties hasData and hasError to check whether it contains a non-null data value or error value, respectively.

Now we can see the basic pattern of using FutureBuilder. When creating a new FutureBuilder object, we pass the Future object as the asynchronous computation to deal with. In the builder function, we check the value of connectionState and return different Widgets using data or error in the AsyncSnapshot.

"HTTP Request Sender" Example

It's better to explain how FutureBuilder works with an example. The example used in this article is very simple. It has a TextField to input URLs. After finishing editing the URL, it uses http to get the content of the web page and display in a Text widget. A loading spinner is displayed when the HTTP request is in progress. Errors are displayed in red text.

Below is the screenshot of this example when the response is displayed.

Screenshot

Now we take a look at how this example is built. Here we ignore details of using other widgets, only code related to FutureBuilder is discussed.

URL input

URLInput shown in the code below is the widget to input URLs. It's a stateless widget with a TextField in it. SetUrl is the function type that accepts a URL of type String. The onSubmitted callback of the TextField is simply set to the passed-in SetUrl function. When the user finishes editing the URL, the function setUrl is invoked with the value the TextField.

typedef SetUrl = Function(String url);

class URLInput extends StatelessWidget {
  URLInput(this.setUrl, {Key key}) : super(key: key);

  final SetUrl setUrl;

  
  Widget build(BuildContext context) {
    return new Container(
      padding: const EdgeInsets.all(10.0),
      child: new Row(
        children: <Widget>[
          new Expanded(child: new TextField(
            onSubmitted: setUrl,
          ))
        ],
      ),
    );
  }
}

Request sender

RequestSender shown in the code below is the widget to send HTTP requests and display results. It has a property url as the URL to send request. In the FutureBuilder, if the url is not null, we use http.get(url).then((response) => response.body) to create the Future object to connect this FutureBuilder to; if the url is null, then the property future is set to null. In the builder function, we check the value of snapshot.connectionState and return different widgets based on the state.

  • ConnectionState.none - This means the future is null, so it returns a Text widget to prompt the user to input a URL.
  • ConnectionState.waiting - This means the HTTP request is in progress, so it returns a CircularProgressIndicator widget to show the spinner.
  • ConnectionState.active - This should not happen for a Future, see more details below. It returns a Text widget with empty text.
  • ConnectionState.done - This means the HTTP request is completed. If hasError is true, it returns a Text widget with the error message in red text; otherwise, it returns a Text widget with the response content wrapped in a ListView for scrolling.
class RequestSender extends StatelessWidget {
  RequestSender(this.url, {Key key}) : super(key: key);

  final String url;

  
  Widget build(BuildContext context) {
    return new Container(
        padding: const EdgeInsets.all(10.0),
        child: FutureBuilder(
            future: url != null
                ? http.get(url).then((response) => response.body)
                : null,
            builder: (BuildContext context, AsyncSnapshot<String> snapshot) {
              switch (snapshot.connectionState) {
                case ConnectionState.none:
                  return new Text('Input a URL to start');
                case ConnectionState.waiting:
                  return new Center(child: new CircularProgressIndicator());
                case ConnectionState.active:
                  return new Text('');
                case ConnectionState.done:
                  if (snapshot.hasError) {
                    return new Text(
                      '${snapshot.error}',
                      style: TextStyle(color: Colors.red),
                    );
                  } else {
                    return new ListView(
                        children: <Widget>[new Text(snapshot.data)]);
                  }
              }
            }));
  }
}

AsyncSnapshot is not only used by FutureBuilder. It's also used by StreamBuilder which works with Streams. A Stream may have multiple values, while a Future can only have one value. ConnectionState.active is used to indicate that a Stream is actively emitting values. That's why ConnectionState.active doesn't apply to Futures.

Send request page

The last part is the SendRequestPage widget that wraps both URLInput and RequestSender. SendRequestPage is a stateful widget with the URL as the state. The function _setUrl updates the state to use the new URL. _setUrl is passed to URLInput as the callback to invoke. RequestSender is created based on the value of _url.

class SendRequestPage extends StatefulWidget {
  SendRequestPage({Key key, this.title}) : super(key: key);

  final String title;

  
  _SendRequestState createState() => new _SendRequestState();
}

class _SendRequestState extends State<SendRequestPage> {
  String _url;

  void _setUrl(String url) {
    setState(() {
      _url = url;
    });
  }

  
  Widget build(BuildContext context) {
    return new Scaffold(
      appBar: new AppBar(
        title: new Text(widget.title),
      ),
      body: new Center(
        child: new Column(
          children: <Widget>[
            new URLInput(_setUrl),
            new Expanded(child: new RequestSender(_url))
          ],
        ),
      ),
    );
  }
}

Summary

This article show the basics of using FutureBuilder to build UI connected to a Future. To use FutureBuilder, we need to do two things:

  • Create the Future object as the value of property future.
  • Build the UI by returning different widgets based on the value of connectionState.

Source code

Source code of this article is available on GitHub.