Explain Dart's Stream and when to use it
4 min read
Dart Fundamentals
A Future is one async value; a Stream is many async values over time — think async iterator. Use streams whenever data trickles in: WebSocket messages, sensor readings, Firestore updates, search input, BLoC state.
Future<T> | Stream<T> | |
|---|---|---|
| Values emitted | Exactly one | Zero, one, or many |
| Completes | Once | When close() is called (or never) |
| Consume with | await | await for / .listen() |
| Typical use | HTTP call, DB query | WebSocket, taps, real-time updates |
A stream has two ends: a sink (where you push values in) and the stream (where consumers listen). It can finish with onDone or blow up with onError.
Code in action
// Creating a stream — 3 common ways
// 1. async* generator (most ergonomic)
Stream<int> countDown(int from) async* {
for (var i = from; i >= 0; i--) {
yield i;
await Future.delayed(const Duration(seconds: 1));
}
}
// 2. Stream.periodic — emit on an interval
final ticker = Stream.periodic(const Duration(seconds: 1), (i) => i).take(5);
// 3. StreamController — when you need manual push
final ctrl = StreamController<String>();
ctrl.sink.add('Hello');
ctrl.sink.add('World');
await ctrl.close();
// Consuming a stream
// listen() — most flexible
final sub = stream.listen(
(data) => print('got $data'),
onError: (e) => print('error: $e'),
onDone: () => print('stream closed'),
);
await sub.cancel(); // remember to cancel!
// await for — reads top-to-bottom
await for (final value in stream) {
print(value);
}
Single-subscription vs Broadcast
// Default: single-subscription — only ONE listener ever
final s = StreamController<int>();
s.stream.listen(print);
s.stream.listen(print); // ❌ throws — already listened
// Broadcast — multiple listeners, no replay of past events
final b = StreamController<int>.broadcast();
b.stream.listen((v) => print('A: $v'));
b.stream.listen((v) => print('B: $v')); // ✅ both receive
Single = one consumer, buffers events. Broadcast = many consumers, fire-and-forget.
Real-world: search with debounce
Stream<List<Result>> liveSearch(Stream<String> queries) => queries
.debounceTime(const Duration(milliseconds: 300)) // wait for typing pause
.distinct() // ignore repeats
.where((q) => q.length >= 2) // skip short queries
.asyncMap(api.search); // call API
This is why streams shine for input — chaining transforms is far cleaner than orchestrating Futures by hand.
When to reach for a Stream vs a Future
| Situation | Pick |
|---|---|
| One-shot async value (HTTP GET, DB read) | Future |
| Data arrives over time (sockets, sensors, file chunks) | Stream |
| Reacting to user input (search, scroll) | Stream |
| State management (BLoC, Riverpod async) | Stream |
| One value but pushed asynchronously to many listeners | Future + a broadcast cache |
Common mistakes to avoid
// ❌ Forgetting to cancel subscriptions → memory leak
class _MyState extends State<MyWidget> {
@override
void initState() {
super.initState();
stream.listen(handleData); // never cancelled!
}
}
// ✅ Hold the subscription and cancel in dispose()
class _MyState extends State<MyWidget> {
late final StreamSubscription _sub;
@override
void initState() {
super.initState();
_sub = stream.listen(handleData);
}
@override
void dispose() {
_sub.cancel();
super.dispose();
}
}
// ❌ Listening twice on a single-subscription stream → StateError
// ✅ Use .asBroadcastStream() or a broadcast controller
// ❌ Forgetting to close a StreamController → leaks + onDone never fires
// Always call controller.close() (often in dispose())
// ❌ Using a raw Stream in the UI when StreamBuilder would do
// setState on every event = manual subscription bookkeeping
Interview follow-ups
-
When would you use a Stream instead of a Future? Any time data arrives over time — WebSocket messages, user input, sensor data, real-time DB updates (Firestore), or in state management where the UI reacts to state changes.
-
What's the difference between
Stream.listen()andawait for?listen()is non-blocking, supportsonError/onDone/cancelOnError, and returns a cancellable subscription.await forblocks the enclosing async function until the stream closes — clean but you can't easily cancel mid-loop or run other work concurrently. -
Single-subscription vs broadcast — when does it matter? Single-subscription buffers events until the (one) listener attaches; great for one-time consumers like HTTP chunks. Broadcast doesn't buffer; late listeners miss earlier events — use it for UI events, app-wide channels, etc.
-
What is
async*and how is it different fromasync?asyncreturns aFuture<T>.async*returns aStream<T>and usesyield(one value) oryield*(delegate to another stream) to emit. It's the cleanest way to write a custom stream.
How helpful was this content?
Please sign in to rate this article.