Flutter (54): Asynchronous UI updates (FutureBuilder, StreamBuilder)

Time: Column:Mobile & Frontend views:231

Many times, we rely on asynchronous data to dynamically update the UI. For example, when opening a page, we might need to fetch data from the internet first. During this data retrieval, we display a loading indicator, and once the data is fetched, we render the page. Similarly, we may want to show the progress of a stream (like file streams or internet data reception streams). Of course, we can achieve all these functionalities using a StatefulWidget. However, since the scenario of relying on asynchronous data to update the UI is very common in actual development, Flutter provides two components, FutureBuilder and StreamBuilder, to quickly implement such features.

54.1 FutureBuilder

FutureBuilder depends on a Future and dynamically builds itself based on the state of the dependent Future. Let's take a look at the FutureBuilder constructor:

FutureBuilder({
  this.future,
  this.initialData,
  required this.builder,
})
  • future: The Future that the FutureBuilder depends on, typically an asynchronous task that takes time.

  • initialData: The initial data that the user sets as default data.

  • builder: A widget builder; this builder will be called multiple times during the different stages of the Future execution, with the following signature:

Function(BuildContext context, AsyncSnapshot snapshot)

The snapshot will contain information about the current status and results of the asynchronous task. For instance, you can use snapshot.connectionState to get the status of the asynchronous task and snapshot.hasError to check if there are any errors. Readers can refer to the AsyncSnapshot class definition for complete details.

Additionally, the builder function signature of FutureBuilder is the same as that of StreamBuilder.

Example

Let's implement a route that fetches data from the internet when it opens. While the data is being retrieved, we will show a loading indicator; once the retrieval is complete, we will display the data if successful, or an error if it fails. Since we have not yet introduced how to make network requests in Flutter, we will simulate this process by returning a string after a 3-second delay:

Future<String> mockNetworkData() async {
  return Future.delayed(Duration(seconds: 2), () => "I am data fetched from the internet.");
}

The FutureBuilder usage code is as follows:

...
Widget build(BuildContext context) {
  return Center(
    child: FutureBuilder<String>(
      future: mockNetworkData(),
      builder: (BuildContext context, AsyncSnapshot snapshot) {
        // Request has ended
        if (snapshot.connectionState == ConnectionState.done) {
          if (snapshot.hasError) {
            // Request failed, show error
            return Text("Error: ${snapshot.error}");
          } else {
            // Request succeeded, show data
            return Text("Contents: ${snapshot.data}");
          }
        } else {
          // Request not finished, show loading
          return CircularProgressIndicator();
        }
      },
    ),
  );
}

The running result is shown in Figures :

Flutter (54): Asynchronous UI updates (FutureBuilder, StreamBuilder)

Note: In the example code, each time the widget is rebuilt, a new request will be initiated because the future is new each time. In practice, we usually implement caching strategies. A common approach is to cache the future after a successful call, so that the asynchronous task will not be re-initiated on the next build.

In the above code, we return different widgets in the builder based on the current asynchronous task status ConnectionState. ConnectionState is an enumeration defined as follows:

enum ConnectionState {
  /// No asynchronous task is currently running, e.g., when [FutureBuilder]'s [future] is null
  none,

  /// Asynchronous task is in a waiting state
  waiting,

  /// Stream is in an active state (data is being transmitted on the stream); this state does not exist for FutureBuilder.
  active,

  /// Asynchronous task has completed.
  done,
}

Note that ConnectionState.active only appears in StreamBuilder.

54.2 StreamBuilder

We know that in Dart, Stream is also used to receive asynchronous event data. Unlike Future, it can receive multiple results from asynchronous operations. It is commonly used in scenarios where data is read multiple times from asynchronous tasks, such as network downloads or file read/write operations. StreamBuilder is specifically used to display UI components that reflect changes in data events on a stream. Below is the default constructor for StreamBuilder:

StreamBuilder({
  this.initialData,
  Stream<T> stream,
  required this.builder,
})

As you can see, the only difference from the FutureBuilder constructor is that the former requires a future, while the latter requires a stream.

Example

Let’s create a timer example that increments a count every second. Here, we use Stream to generate a number every second:

Stream<int> counter() {
  return Stream.periodic(Duration(seconds: 1), (i) {
    return i;
  });
}

The StreamBuilder usage code is as follows:

Widget build(BuildContext context) {
    return StreamBuilder<int>(
      stream: counter(),
      //initialData: ,// a Stream<int> or null
      builder: (BuildContext context, AsyncSnapshot<int> snapshot) {
        if (snapshot.hasError)
          return Text('Error: ${snapshot.error}');
        switch (snapshot.connectionState) {
          case ConnectionState.none:
            return Text('No Stream');
          case ConnectionState.waiting:
            return Text('Waiting for data...');
          case ConnectionState.active:
            return Text('Active: ${snapshot.data}');
          case ConnectionState.done:
            return Text('Stream is closed');
        }
        return null; // unreachable
      },
    );
}

Readers can run this example themselves to see the results. Note that this example is just to demonstrate the use of StreamBuilder. In practice, any UI that relies on multiple asynchronous data sources can utilize StreamBuilder.