Asynchronous Programming in Dart and Flutter

The UI widgets available in the Flutter framework use Dart's asynchronous programming features to great effect, helping to keep your code organized and preventing the UI from locking up on the user. In this article, we'll look at how asynchronous code patterns can help with processing user interaction and retrieving data from a network, then we'll see a few asynchronous Flutter widgets in action.

The code for this article was tested with Dart 2.8.4 and Flutter 1.17.5.
Note: In order to get the most out of this article, it's best to be familiar with the concepts detailed in the Asynchrony Primer for Dart and Flutter.

User interaction events

Perhaps the simplest example for asynchronously processing user input is responding to interaction events on a button widget with callbacks:

FlatButton(
  child: Text("Get Data"),
  onPressed: () {
    print("Button pressed.");
  },
)

The FlatButton widget, like most button-like Flutter widgets, provides a convenience parameter called onPressed for responding to button presses. Here, we've passed an anonymous callback function to the parameter that does nothing aside from printing a message to the console. When the user presses the button, the onPressed event is triggered, and the anonymous function will be executed as soon as the event loop can get to it.

Behind the scenes there is an event stream, and each time a new event is added to it, your callback function is called with any pertinent data. In this case, a simple button press has no associated data, so the callback takes no parameters.

Let's look at other places where we need to use asynchronous code to interact with the framework and core libraries.

Asynchronous network calls using callbacks

One of the most common cases for asynchronous programming involves getting data over a network, such as through a REST service over HTTP:

import 'package:http/http.dart' as http;

final future = http.get("https://example.com");

future.then((response) {
  if (response.statusCode == 200) {
    print("Response received.");
  }
});

The http package is among the most popular on Dart's package repository, Pub. I've included the import statement here to point out the typical pattern of namespacing the import with the name http using the as keyword. This helps keep the package's many top-level functions, constants, and variables from clashing with your code, as well as making it clear where functions like get() come from.

The code example shows the classic pattern for consuming a future. The call to http.get() immediately returns an incomplete Future instance when called. Remember that acquiring results over HTTP takes time, and we don't want our app to be unresponsive while we wait. That's why we get the future back right away and carry on with executing the next lines of code. Those next lines use the Future instance's then() method to register a callback that will be executed when the REST response comes in at some point in the future. If the eventual response has an HTTP status code of 200 (success), we print a simple message to the debug console.

Let's refine this pattern a bit. That example stores the future in a final variable in order to access then(), but unless you have a good reason to keep that future instance around, it's typical to skip that part, as in the following example:

http.get("https://example.com").then((response) {
  if (response.statusCode == 200) {
    print("Response received.");
  }
  else {
    print("Bad response.");
  }
});

Since the call to get() resolves to a Future, you can call its then() method on it directly, without saving the future reference in a variable. The code is a bit more compact this way, but still readable.

It's possible to chain several useful callback registrations onto our future, like so:

http.get("https://example.com").then((response) {
  if (response.statusCode == 200) {
    print("Response received.");
  }
  else {
    print("Bad response.");
  }
}).catchError(() {
  print("Error!");
}).whenComplete(() {
  print("Future complete.");
});

Now we've registered a callback to be executed when the HTTP call ends with an error instead of a response using catchError(), and another that will run regardless of how the future completes using whenComplete(). This method chaining is possible because each of those methods returns a reference to the future we're working with.

For most, registering callbacks may be the most familiar pattern for dealing with futures, but there is another way.

Asynchronous network calls without callbacks

Dart offers an alternate pattern for making asynchronous calls, one that looks more like regular synchronous code, which can make it easier to read and reason about. The async/await syntax handles a lot of the logistics of futures for you:

Future<String> getData() async {
  final response = await http.get("https://example.com");
  return response.body;
}

When you know you'll be performing an asynchronous call within a function, such as http.get(), you can mark your function with the async keyword. An async function always returns a future, and you can use the await keyword within its body. In this case, we know the REST call will return string data, so we use generics on our return type to specify this: Future<String>.

You can await any function that returns a future. The getData() function will suspend execution immediately after the await expression runs and return a future to the caller. The code waits for a response; it waits for the network call's future to complete. Later, when the response comes in over the network, execution resumes and the Response object is assigned to the final variable, then getData() returns response.body, which is a string. You don't need to explicitly return a future from getData(), because one is automatically returned on the first use of await. Once you have the string data, you return that, and Dart completes the future with the value.

This function reads like a synchronous function, which is nice for our limited human brains, but it executes asynchronously. It's also less verbose than registering callbacks.

To catch errors when using await, you can use Dart's standard try/catch feature:

Future<String> getData() async {
  try {
    final response = await http.get("https://example.com");
    return response.body;
  } catch (exc) {
    print("Error: $exc");
  }
}

In this version, we place code that could throw exceptions into the try block. If everything goes smoothly, we'll get a response and return the string data, just as in the prior example. In the event of an error, the catch block will execute instead, and we'll be passed a reference to the exception. Since we haven't added an explicit return statement to the end of getData(), Dart will add an implicit return null statement there, which will complete the future with a null value. Note that if the network call succeeds, the return happens in the try block, so the implicit return won't be invoked.

Of course, you should check the status code of REST responses, but I've omitted that here for brevity.

Callbacks have their uses, and they can be a great way to handle asynchronous communication for simple cases, such as responding to a user pressing a button. For more complicated scenarios, such as when you need to make several asynchronous calls in sequence, with each depending on the results of the prior call, Dart's async/await syntax can help you avoid nesting callbacks, a situation sometimes referred to as callback hell.

Let's look at a few Flutter widgets that can help when working with asynchronous calls.

The FutureBuilder widget

A FutureBuilder widget builds itself based on the state of a given future. For this example, let's assume you have a function called getData() that returns a Future<String>. Many developers start their experimentation with this widget using code something like this:

class MyStatelessWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: getData(),
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }

        if (snapshot.hasData) {
          return Text(snapshot.data);
        }

        return Container();
      },
    );
  }
}

This custom stateless widget returns a FutureBuilder that will display a progress indicator if the future returned by getData() has not yet completed, and it will show the data if the future has completed with a value. If neither of those things is true, an empty Container is rendered instead. You tell the FutureBuilder which future to watch with its future parameter, then give it a builder function that will be called for every rebuild. The builder callback receives the usual BuildContext argument common to all Flutter build operations, and it also takes an instance of AsyncSnapshot, which you can use to check the future's status and retrieve any data.

There is a problem with this approach. According to the official documentation for FutureBuilder, the provided future needs to have been obtained prior to the build step. Otherwise the asynchronous call for data will be repeatedly executed, essentially starting over with every build. Flutter widgets can be rebuilt at any time for many reasons, including animation or user interaction, so the builder function provided to FutureBuilder could execute many times in a single second. Normally you don't want to retrieve the same data again and again, so you need to make the retrieval call outside the FutureBuilder.

To fix it, we need to use a stateful widget instead:

class MyStatefulWidget extends StatefulWidget {
  @override
  _MyStatefulWidgetState createState() => _MyStatefulWidgetState();
}

class _MyStatefulWidgetState extends State<MyStatefulWidget> {
  Future<String> _dataFuture;

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

    _dataFuture = getData();
  }

  @override
  Widget build(BuildContext context) {
    return FutureBuilder(
      future: _dataFuture,
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }

        if (snapshot.hasData) {
          return Text(snapshot.data);
        }

        return Container();
      },
    );
  }
}

This version of the widget acquires the data future during initState(). The initState() method will be called exactly once when the widget's state object is created. It will not be executed during a widget rebuild. The future is stored in a private member variable, then provided to FutureBuilder. The builder function hasn't changed at all.

There is also a widget for Dart's other pillar of asynchronous communications, the stream.

The StreamBuilder widget

A stream is like an event pipe. Data or error events go in one end, and they are delivered to listeners on the other. When you provide a StreamBuilder with a reference to an existing stream, it automatically subscribes and unsubscribes to updates as necessary, and it builds itself based on any data that comes down the pipe. It's very similar to the FutureBuilder widget, the difference being that streams may deliver data periodically instead of only once.

With this widget, you can set up a part of your UI that will update whenever new data becomes available:

class MyStatelessWidget extends StatelessWidget {
  final Stream<String> dataStream;

  const MyStatelessWidget({Key key, this.dataStream}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StreamBuilder<ConnectionState>(
      stream: dataStream,
      builder: (BuildContext context, AsyncSnapshot<ConnectionState> snapshot) {
        if (snapshot.connectionState == ConnectionState.waiting) {
          return CircularProgressIndicator();
        }

        if (snapshot.hasData) {
          return Text(snapshot.data);
        }

        return Container();
      },
    );
  }
}

This custom stateless widget accepts a stream as a constructor parameter. The stream is passed along to an instance of StreamBuilder in the widget's build() method. The builder function is run for each new value in the stream, and it displays a progress indicator if the stream is in a waiting state. If there is already data to show off, it's displayed in a Text widget. Otherwise an empty Container is rendered.

Conclusion

We've seen how you can use asynchronous patterns to interact with Flutter framework code and Dart's core libraries, which will help you get the most out of those tools. For further reading, check out these related articles: