Streams Basics
Handling a sequence of asynchronous values over time with Stream, and consuming one with await for.
3 min de lecture
A Future represents one value that arrives eventually. A Stream represents several values arriving over time — think of it as the asynchronous equivalent of an Iterable: instead of a list you loop over all at once, a stream hands you elements one at a time as they become available.
Where streams show up
Button clicks, incoming WebSocket messages, periodic timers, and chunks of a file being read all fit this shape naturally — an unknown number of values, spaced out over time, rather than one value or a fixed list you already have in memory.
Creating a stream
Stream<int> countTo(int max) async* {
for (int i = 1; i <= max; i++) {
await Future.delayed(const Duration(milliseconds: 500));
yield i;
}
}async* marks this as a generator function that produces a Stream rather than a Future. yield emits one value into the stream and continues the function afterward — contrast this with return, which would end the function immediately after the first value.
Listening to a stream
There are two common ways to consume a stream. The first is .listen(), which registers a callback for every value as it arrives:
void main() {
countTo(5).listen(
(value) => print('Got: $value'),
onDone: () => print('Stream finished'),
onError: (error) => print('Error: $error'),
);
}The second is await for, which reads more like a normal loop and is often clearer inside an async function:
Future<void> printCount() async {
await for (final value in countTo(5)) {
print('Got: $value');
}
print('Stream finished');
}
void main() {
printCount();
}await for pauses the surrounding async function on each iteration until the next value arrives (or the stream closes), which is why it can only be used inside a function marked async, just like a single await.
Transforming a stream
Streams support many of the same methods as Iterable — .map(), .where(), and friends — except each one still produces values over time rather than all at once:
Future<void> printEvens() async {
final evens = countTo(10).where((n) => n.isEven);
await for (final value in evens) {
print(value); // 2, 4, 6, 8, 10 — each one as it's produced
}
}Single-subscription vs. broadcast streams
By default, a stream can only be listened to once — a second .listen() call throws a StateError. If multiple listeners genuinely need the same events (say, several parts of a UI reacting to the same event source), convert it to a broadcast stream:
final controller = StreamController<int>.broadcast();
controller.stream.listen((value) => print('Listener A: $value'));
controller.stream.listen((value) => print('Listener B: $value'));
controller.add(1);
controller.add(2);StreamController is how you create and feed a stream manually — useful when the values come from something other than a generator function, like a UI event or an external callback you're wrapping.
Streams are a deep topic on their own, but the core idea carries you far: a Future is one value later, a Stream is many values over time, and await for lets you consume the second exactly as naturally as await lets you consume the first.