A Stream in Dart is used to handle a sequence of asynchronous events — rather than a single future value like a Future, a Stream provides multiple values over time. You can think of it as a continuous data pipeline that delivers data as it becomes available.
For example, if you’re listening to live data such as sensor readings, chat messages, or user input changes, a Stream is the perfect choice because it can emit multiple values asynchronously.
There are two main types of Streams in Dart:
- Single-subscription streams – These can only be listened to once. Typically used for sequential events like reading a file or receiving an HTTP response chunk-by-chunk.
- Broadcast streams – These can be listened to by multiple listeners simultaneously. Often used for real-time data sources like WebSockets, user input, or event buses.
Here’s a simple example:
Stream numberStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i; // emits one value at a time
}
}
void main() async {
await for (var number in numberStream()) {
print('Received: $number');
}
}
In this example, the stream emits numbers one by one every second, and the await for loop listens to and processes each emitted value as it arrives.
I’ve applied this concept in a Flutter project while implementing a real-time chat feature. The chat messages were coming from a WebSocket connection. Instead of repeatedly polling the server, I used a Stream to listen for incoming messages. Each time a new message event arrived, the UI automatically updated using Flutter’s StreamBuilder widget:
StreamBuilder(
stream: chatMessageStream,
builder: (context, snapshot) {
if (!snapshot.hasData) return CircularProgressIndicator();
return Text(snapshot.data);
},
)
This made the app highly responsive to real-time data changes.
One challenge I faced was managing stream subscriptions — especially avoiding memory leaks when widgets were disposed but the stream was still active. To fix that, I ensured to cancel subscriptions inside the dispose() method or used StreamController with proper lifecycle management.
A limitation of streams is that error handling and cancellation need careful attention — if not handled, they can cause background operations to continue unnecessarily.
As an alternative, for more complex use cases like combining or transforming multiple streams, I’ve used the RxDart package, which extends Dart Streams with powerful reactive operators like merge, combineLatest, and debounce.
So overall, Streams are essential for handling continuous, asynchronous data in Dart — especially in Flutter apps where real-time updates or event-driven programming is key.
