In Dart, the Stream class is mainly used to handle asynchronous data that comes over time, like a continuous flow of events or values. Unlike a Future which represents a single async result, a Stream provides a sequence of async events, and itβs perfect whenever I need to listen, react, or respond to changes dynamically.
In practical terms, Iβve used Streams in multiple real-world scenarios β one example is in a chat application, where I used a Stream to listen to new messages coming from Firebase Firestore in real time. Whenever new messages were added, the Stream automatically pushed those updates to the UI without manually refreshing.
Another common use case is handling user input events β for example, in search boxes, where I debounce user typing using Streams, or when working with sensors, location updates, or socket connections that send data continuously.
A simple example would be like this:
Stream countStream() async* {
for (int i = 1; i <= 5; i++) {
await Future.delayed(Duration(seconds: 1));
yield i; // Emits a value every second
}
}
void main() async {
await for (var value in countStream()) {
print('Received: $value');
}
}
Here, the Stream emits a value every second, and the await for loop listens to those emissions one by one β itβs asynchronous but sequential.
In Flutter, I often use Streams with StreamBuilder widgets, which rebuild the UI automatically when new data arrives. For example, in a stock market app, I used StreamBuilder to show live price updates:
StreamBuilder(
stream: stockPriceStream(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text('Current Price: βΉ${snapshot.data}');
} else {
return CircularProgressIndicator();
}
},
);
This keeps the UI reactive without manual refresh logic β every time a new value comes through the stream, the widget updates itself.
One challenge I faced while working with Streams was managing subscriptions properly. If I forget to close or cancel a Stream subscription, it can lead to memory leaks, especially in long-running screens or background tasks. To handle that, I use StreamController.close() in dispose() methods in Flutter, or takeUntil pattern when using RxDart to manage lifecycle-based streams.
Another challenge is error handling β Streams can emit errors mid-way, so I always handle it using onError or by checking snapshot.hasError inside StreamBuilder.
In terms of limitations, Dart Streams work great for single-subscription data, but if I need to allow multiple listeners, I use broadcast streams by calling .asBroadcastStream(). However, managing backpressure (too many events at once) can still be tricky, so I sometimes use RxDart, which provides advanced operators like debounce, combineLatest, and throttle β very useful for complex reactive patterns.
