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 theFutureBuilder
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 :
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
.