This tutorial shows you how to use StreamBuilder
in Flutter.
An asynchronous process may need some time to finish. Sometimes, there can be some values emitted before the process finishes. In Dart, you can create a function that returns a Stream
, which can emit some values while the asynchronous process is active. If you want to build a widget in Flutter based on the snapshots of a Stream
, there's a widget called StreamBuilder
. This tutorial explains how to use the widget.
Using StreamBuilder
Widget
To use StreamBuilder
, you need to call the constructor below.
const StreamBuilder({
Key? key,
Stream<T>? stream,
T? initialData,
required AsyncWidgetBuilder<T> builder,
})
Basically, you need to create a Stream
and pass it as the stream
argument. Then, you have to pass an AsyncWidgetBuilder
which can be used to build the widget based on the snapshots of the Stream
.
Create Stream
Below is a simple function that returns a Stream
for generating numbers every one second. You need to use the async*
keyword for creating a Stream
. To emit a value, you can use yield
keyword followed by the value to be emitted.
Stream<int> generateNumbers = (() async* {
await Future<void>.delayed(Duration(milliseconds: 2));
for (int i = 1; i <= 5; i++) {
await Future<void>.delayed(Duration(milliseconds: 1));
yield i;
}
})();
After that, pass it as the stream
argument.
StreamBuilder<int>(
stream: generateNumbers,
// other arguments
)
Create AsyncWidgetBuilder
The constructor requires you to pass a named argument builder
whose type is AsyncWidgetBuilder
. It's a function with two parameters whose types in order are BuildContext
and AsyncSnapshot<T>
. The second parameter which contains the current snapshot of the Stream
can be used to determine what should be rendered.
To create the function, you need to understand about AsyncSnapshot
first. The AsyncSnapshot
is an immutable representation of the most recent interaction with an asynchronous computation. In this context, it represents the latest interaction with a Stream
. You can access the properties of AsyncSnapshot
to get the latest snapshot of the Stream
. One of the properties that you may need to use is connectionState
, an enum whose value represents the current connection state to an asynchronous computation which is a Stream
in this context. The enum has some possible values:
none
: Not connected to any asynchronous computation. It can happen if thestream
is null.waiting
: Connected to an asynchronous computation and awaiting interaction. In this context, it means theStream
hasn't completed.active
: Connected to an active asynchronous computation. For example, if aStream
has returned any value but not completed yet.done
: Connected to a terminated asynchronous computation. In this context, it means theStream
has completed.
AsyncSnapshot
also has a property named hasError
which can be used to check whether the snapshot contains a non-null error value. The hasError
value will be true if the latest result of the asynchronous operation was failed.
For accessing the data, first you can check whether the snapshot contains data by accessing its hasData
property which will be true if the Stream
has already emitted any non-null value. Then, you can get the data from the data
property of AsyncSnapshot
.
Based on the values of the properties above, you can determine what should be rendered on the screen. In the code below, a CircularProgressIndicator
is displayed when the connectionState
value is waiting
. When the connectionState
changes to active
or done
, you can check whether the snapshot has error or data.
StreamBuilder<int>(
stream: generateNumbers,
builder: (
BuildContext context,
AsyncSnapshot<int> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return CircularProgressIndicator();
} else if (snapshot.connectionState == ConnectionState.active
|| snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors.teal, fontSize: 36)
);
} else {
return const Text('Empty data');
}
} else {
return Text('State: ${snapshot.connectionState}');
}
},
)
The builder
function is called at the discretion of the Flutter pipeline. Therefore, it will receive a timing-dependent sub-sequence of the snapshots. That means if there are some values emitted by the Stream
at almost the same time, there's a possibility that some of the values are not passed to the builder.
Set Initial Data
You can optionally pass a value as the initialData
argument which will be used until the Stream
emits a value. If the passed value is not null, the hasData
property will be true
initially even when the connectionState
is waiting
.
StreamBuilder<int>(
initialData: 0,
// other arguments
)
In order to show the initial data when the connectionState
is waiting, the if (snapshot.connectionState == ConnectionState.waiting)
block in the code above needs to be modified.
if (snapshot.connectionState == ConnectionState.waiting) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
Visibility(
visible: snapshot.hasData,
child: Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors.black, fontSize: 24),
),
)
],
);
}
StreamBuilder
- Parameters
Key? key
: The widget's key, used to control how a widget is replaced with another widget.Stream<T>? stream
: AStream
whose snapshot can be accessed by thebuilder
function.T? initialData
: The data that will be used to create the initial snapshot.required AsyncWidgetBuilder<T> builder
: The build strategy used by this builder.
Full Code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Woolha.com Flutter Tutorial',
home: StreamBuilderExample(),
debugShowCheckedModeBanner: false,
);
}
}
Stream<int> generateNumbers = (() async* {
await Future<void>.delayed(Duration(seconds: 2));
for (int i = 1; i <= 5; i++) {
await Future<void>.delayed(Duration(seconds: 1));
yield i;
}
})();
class StreamBuilderExample extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _StreamBuilderExampleState ();
}
}
class _StreamBuilderExampleState extends State<StreamBuilderExample> {
@override
initState() {
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Woolha.com Flutter Tutorial'),
),
body: SizedBox(
width: double.infinity,
child: Center(
child: StreamBuilder<int>(
stream: generateNumbers,
initialData: 0,
builder: (
BuildContext context,
AsyncSnapshot<int> snapshot,
) {
if (snapshot.connectionState == ConnectionState.waiting) {
return Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
Visibility(
visible: snapshot.hasData,
child: Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors.black, fontSize: 24),
),
),
],
);
} else if (snapshot.connectionState == ConnectionState.active
|| snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasError) {
return const Text('Error');
} else if (snapshot.hasData) {
return Text(
snapshot.data.toString(),
style: const TextStyle(color: Colors.teal, fontSize: 36)
);
} else {
return const Text('Empty data');
}
} else {
return Text('State: ${snapshot.connectionState}');
}
},
),
),
),
);
}
}
Summary
If you need to build a widget based on the result of a Stream
, you can use the StreamBuilder
widget. You can create a Stream
and pass it as the stream
argument. Then, you have to pass an AsyncWidgetBuilder
function which is used to build a widget based on the snapshots of the Stream
.
You can also read our tutorials about:
FutureBuilder
: A widget that builds itself based on the snapshots of aFuture
.