Hello everyone! In this article, I will explain the use of Stream in Flutter through an example.

What is a Stream?

We can define it as a pipeline with a flow of data. There is a circulation of data in the stream. Data enters the flow and is delivered to those who want to listen to it.

Let’s Create a Stream!

To create a Stream, we first need a Controller. We will create a Stream using StreamController. This way, we can facilitate data communication over the Stream.

late final StreamController<T> _streamController = StreamController<T>();

We can access the Stream through the StreamController we created.

 final stream = _streamController.stream;

If we want to listen for changes in the created stream, we can do it like this:

 //_streamController.stream.listen(print);  
stream.listen(print);

If we want to send a value in the stream, we can do it like this:

 var sendedValue = '';
_streamController.sink.add(sendedValue);

Now, let’s see how to listen to a Stream that refreshes the UI.

StreamBuilder<T>(
        // Set initial value.
        initialData:  'Hello, this is initial value.',
        // Open a stream through the StreamController.
        stream: _streamController.stream,
        // When there is a change, the widget builder will be rebuilt.
        builder: (context, AsyncSnapshot<T> snapshot) { },)

Now that we have learned to create and listen to streams, let’s look at an example. In this example, we will create a TextFormField and see the changes in the input displayed in a Text widget.

First, we define our StreamController, and we declare it as late final to initialize it. Since the value we will listen to is of type String, we give the Stream the generic type String.

// We define StreamController to create a Stream.
late final StreamController<String> _streamController;

We also define a TextEditingController for the TextFormField.

late final TextEditingController _textEditingController;

In the widget’s init, we initialize our StreamController and TextEditingController.

@override
void initState() {
    super.initState();
    // Initialize the StreamController.
    _streamController = StreamController<String>();
    _textEditingController = TextEditingController();
}

Now we can create the widget structure. We define a StreamBuilder and pass our stream from the StreamController to the stream parameter. This way, we will trigger changes in the Text widget based on the updates.

StreamBuilder<String>(
        // Set the initial value.
        initialData:
            'Hello, this field will change when there is a change in the TextFormField.',
        // Open a stream through the StreamController.
        stream: _streamController.stream,
        // When there is a change, the widget builder will be rebuilt.
        builder: (context, AsyncSnapshot<String> snapshot) {
          return SizedBox();
        })

The data is in snapshot format. We can access the data with snapshot.data.

StreamBuilder<String>(
        // Set the initial value.
        initialData:
            'Hello, this field will change when there is a change in the TextFormField.',
        // Open a stream through the StreamController.
        stream: _streamController.stream,
        // When there is a change, the widget builder will be rebuilt.
        builder: (context, AsyncSnapshot<String> snapshot) {
          return Text(snapshot.data ?? '');
        })

Let’s create our TextFormField under the StreamBuilder and provide it with the Controller.

TextFormField(controller: _textEditingController, ),

Now, let’s define the onChanged function for the TextFormField and send the changes to the Stream through the StreamController.

TextFormField(
              controller: _textEditingController,
              onChanged: (val) {
                // Notify the changes to the stream.
                _streamController.sink.add(val);
              },
            ),

Don’t forget to dispose of our Controllers when we are done.

@override
void dispose() {
    // Don't forget to close it when we're done!
    _streamController.close();
    _textEditingController.dispose();
    super.dispose();
}

That’s it! You can run the project and observe the changes in the text.

You can access the full project here.

Happy coding to everyone! 🙃