Flutter - Using StreamBuilder Widget Examples

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 the stream is null.
  • waiting: Connected to an asynchronous computation and awaiting interaction. In this context, it means the Stream hasn't completed.
  • active: Connected to an active asynchronous computation. For example, if a Stream has returned any value but not completed yet.
  • done: Connected to a terminated asynchronous computation. In this context, it means the Stream 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: A Stream whose snapshot can be accessed by the builder 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 a Future.