If you’re using file.open.read method to read data from a file, it returns a stream. Chunks of data are read from the disk and arrive at the event loop and add the data to the stream, and it pops out in your app’s code. When another piece of data arrives, in it goes, and out it comes. Timer-based streams, streaming data from a network socket– They work with the event loop too using the clock and network events.

In this tutorial, I’m going to cover streams. Each Future represents a single value, either an error or data that it delivers asynchronously while Streams work similarly, only instead of a single thing, They can deliver zero or more values and errors over time.

Flutter StreamBuilder

Create Streams

Let’s create streams of your own. Just like with futures, most of the time, you’re going to be working with streams created for you by network libraries, file libraries, state management, and so on. But you can make your own as well using a StreamController.

  static Stream<int> StreamCreater(Duration interval, [int maxCount]) {
    StreamController<int> controller;
    Timer timer;
    int count = 0;

    void increment_count(_) {
      count++;

      if (count == maxCount) {
        timer.cancel();
        controller.sink.close(); 
      } else if (count == 3) {
        controller.sink.addError('Error !');
      } else if (count < maxCount) {
        controller.sink.add(count);
      }
    }

    void startTimer() {
      timer = Timer.periodic(interval, increment_count);
    }

    void stopTimer() {
      if (timer != null) {
        timer.cancel();
        timer = null;
      }
    }

    controller = StreamController<int>(
        onListen: startTimer,
        onPause: stopTimer,
        onResume: startTimer,
        onCancel: stopTimer);

    return controller.stream;
  }

Here’s the actual code for it. As you can see, it keeps a running count, and it uses a timer to increment that count each second.

StreamController

A StreamController creates new Streams from scratch and gives you access to both ends of it. And there’s the sink, which is where new data gets added to the stream.

Subscribe to Stream

Let’s talk about how to work with data provided by a stream. Say I have a method that will give me a stream that kicks out a new integer one per second– like one, two, three, four, five. I can use the listen method to scribe to the stream. I give it a function, and every time a new value is emitted by the stream, my function gets called and prints it. That’s how listen works.

subscription = StreamCreater(const Duration(seconds: 1), 6).listen((data) => print('Data snsns:$data'),
          onError: (err) {
        print('Error $err');
      }, onDone: () {
        print("Done");
      });

Streams can produce errors- By adding an onError method you can catch and process any errors. There’s a onDone method you can use to execute some code when the stream is finished sending data, such as when a file has been completely read.

Broadcast Stream

One important thing to note is that, by default, streams are set up for a single subscription. They hold onto their values until someone subscribes, and they only allow a single listener for their entire lifespan. If you try to list to one twice you’ll get an exception.

StreamCreater(const Duration(seconds: 1), 6).asBroadcastStream();

Dart also offers broadcast streams, you can use the asBroadcastStream method to make a broadcast stream from a single subscription one. They work the same as single subscription streams, but they can have multiple listeners. If nobody’s listening when a piece of data is ready, it gets tossed out.

Manipulate the Streams

Once you’ve got data in a stream, there are a lot of operations that suddenly become fluent and elegant.

StreamCreater(const Duration(seconds: 1), 6).where((i) => i % 2 == 0).map((i) => 'String $i');

I can use a method called map to take each value from the stream and convert it on the fly, into something else. I give the map a function to do the conversion, and it returns a new stream, typed to match the return value of my function.

There are a ton of methods you can chain up like this. If I only want to print the even numbers, for example, I can use where to filter the stream. I give it a function that returns a Boolean for each element, and it returns a new stream that only includes values that pass that test.

Using Text inside the StreamBuilder

Now that we’ve covered creating, manipulating and listening to streams, Let’s talk about how to put them to work with widgets in Flutter.

For Streams, there’s a similar widget called StreamBuilder.Give ita stream, like the one from streamCreater and it will rebuild its children whenever a new value is emitted by the stream.

StreamBuilder<int>(
              stream: StreamCreater(const Duration(seconds: 1), 6),
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return Text("No data yet",style: style,);
                } else if (snapshot.connectionState == ConnectionState.done) {
                  return Text("Done !",style: style,);
                } else if (snapshot.hasError) {
                  return Text(snapshot.error,style: style,);
                } else if (snapshot.hasData) {
                  return Text('${snapshot.data}',style: style,);
                }
              },
            ),

The snapshot parameter is a async snapshot.You can check its connectionState property to see if the stream hasn’t yet sent any data, or if it’s completely finished. And you can use the hasError property to see if the latest value is an error and handle data values as well.