In Flutter to write asynchronous code, you can employ two methods either using Futures or using Streams. Asynchronous code basically allows you to run statements while waiting for another operation to finish. It ensures that the app isn’t blocked and can the UI can be updated while another operation completes. In this post, we will explore Streams in Flutter and how to use them.
Some examples of using Streams are:
- Get constant updates on the audio player. (Playing, Stopped, Paused)
- The user typing on the keyboard is also a stream of character data.
- While login check every 3 seconds if the user is verified.
What are Streams in Flutter? 🐟
Streams are basically a sequence of asynchronous events. The easiest way to conceptualize Streams is to think of it as a pipe, where we put events from one end and receive it from another end.
The Stream can contain a data event or can also contain an error event. When the emission is completed it passes a special done signal which notifies the listeners that no events are left. The benefit of using streams is that it reduces the coupling between frontend and backend. The sender simply puts the values in the stream and is not bothered about the listeners. The receiver just listens to the stream and is not bothered with its implementation. This leads to abstraction on a minor scale.
There are 4 main components in the concept of Streams in Dart:
- Stream: It represents the asynchronous stream of data. Listeners can listen to the stream and be notified of any event updates.
- EventSink: It allows us to add events to the stream. It can be a data event or an error event.
- StreamController: It simplifies using streams in dart. It manages the stream, sink, and manages stream events.
- StreamSubscription: When we subscribe to any stream we get a stream subscription object using which we can pause, resume or cancel the flow of data that we receive.
We won’t be creating our own stream or sink mostly, the StreamController does that for us. When we listen to any stream we must ensure that we save the stream subscription and cancel when the stream is not needed.

You might also be interested in:
Creating a Stream in Flutter🐣
To use streams you simply create a StreamController:
import 'dart:async';
StreamController<int> controller = StreamController<int>();
Now we can easily access the stream from the StreamController object:
Stream stream = controller.stream;
The StreamController internally creates and manages the stream for us. StreamController is like a helper interface provided to developers to use Streams and EventSinks in Flutter.
Adding/Emitting Events on the Stream 🔢
We can add the values to the Stream using the EventSink:
// Adding Items via sink
controller.sink.add(10);
With a StreamController instance, you can easily access the stream of events of a Stream using the listen()
method. You can also access the sink and easily add new data using the add()
method on the sink.
Please note that the StreamController also provides a add()
function that in turn adds the data in the sink. So don’t confuse about the two, both do the same task.
controller.sink.add(10);
//OR
controller.add(10);
To add an error event simply use the addError()
function instead of add()
:
controller.addError("Error Occured!");
How to access values from a Stream ▶
Now that you have created a stream using StreamController, let’s see how to get values from the stream. This is also known as listening or subscribing to the stream. According to the pipe convention, we are fixing the receiving point for the stream of data. We subscribe to the stream simply using the listen()
function.
Stream stream = controller.stream;
stream.listen((value){
print("Value: $value));
});
To listen to the error or done event you can use the StreamSubscription object:
final subscription = controller.stream.listen((int data){
print(data.toString());
}
subscription.onError((e){
print("An error occurred");
});
subscription.onDone((){
subscription.cancel();
// Cancelling the subscription
});
Canceling a stream ❌
When you manually subscribe to a stream, you should ensure that in the dispose method you cancel the StreamSubscription. If you want the stream throughout the duration of your app you may not need to cancel the stream. You can cancel the stream like this:
susbscription.cancel();
Types of Streams in Flutter🌀
By default, Flutter has two types of Streams based on the number of subscriptions it allows.

1. Single Subscription Streams 👤
In this type of stream, only one listener can listen to the stream. Think of it as a drinking straw with one opening and one end. It doesn’t allow subscriptions in between. Used where events need to be delivered in the correct order without missing any of them. Listening again later means missing out on some part of the data, thus it does not provide multi-subscribing ability.
The syntax for Single Subscription Streams:
StreamController<double> controller = StreamController<double>();
// One Listener only
final subscriber1 = controller.stream.listen((e){});
2. Broadcast Streams 👥
In this type of stream, multiple listeners can listen to the stream. You can start to listen to such a stream at any time, and you get events that are emitted once subscribed. Even if you miss out on some previous events it’s not relevant here.
The syntax for Broadcast Streams:
StreamController<double> controller = StreamController<double>.broadcast();
// Multiple Listeners allowed
final subscriber1 = controller.stream.listen((e){});
final subscriber2 = controller.stream.listen((e){});
Methods to Modify a Stream ⚒
Flutter provides inbuilt methods to modify the stream. These methods provide a new stream based on the original stream:
Stream<R> cast<R>();
Stream<S> expand<S>(Iterable<S> Function(T element) convert);
Stream<S> map<S>(S Function(T event) convert);
Stream<T> skip(int count);
Stream<T> skipWhile(bool Function(T element) test);
Stream<T> take(int count);
Stream<T> takeWhile(bool Function(T element) test);
Stream<T> where(bool Function(T event) test);
You can use the following methods like this:
StreamController controller = StreamController();
Stream modifiedStream = controller.stream
..where((event) {
return event.toString().length > 10;
})
..map((event) {
return event.toString().toUpperCase();
});
modifiedStream.listen((event) {
print(event.toString());
});
Now the modified stream will only contain events with a length of more than 10 characters using the where
function and the characters will be capitalized using the map
function. Similarly, other methods can be used to modify the stream.
Modify Data using transform()
function 👨🔧
The transform()
function is an advanced way of modifying a stream. Here you can add your personalized logic as to how to filter, transform the data. We use the transform function like this:
Stream stream = controller.stream;
final transformedStream = stream.transform(StreamTransformer.fromHandlers(
handleData: (data, sink){
// Add Data Modifying logic here
},
handleError: (error, stackTrace, sink){},
handleDone: (sink){},
));
transformedStream.listen((e){
print("The Event is $e");
});
To replicate the above example of length greater than 10 and capitalized letters, the code will be:
Like this, using the transform() method we can easily modify the data. The handleData
acts as a transformer where we can modify the data and then push it into the EventSink. Similarly, if needed we can also modify the error and done events.
Following to use, Streams in Flutter we use StreamBuilder which is an inbuilt widget in Flutter to use Streams.
Conclusion 💫
In this post, I have elaborately explained streams in Flutter & Dart. We learned how to create, listen and cancel a stream. We also took a look at how to modify a stream in Flutter using predefined methods and using the transform()
method. I hope now you can easily use streams in your future apps.
If you have any queries you can comment below and I will be happy to help you. If you are new to Flutter check my post on how to install flutter on windows?