Streams in Dart
By Syncfusion®, Inc. · Last updated
How Streams deliver many values over time: creating them, listening with listen and await for, making your own with StreamController and async*, and changing them with map, where and friends.
What Is a Stream?
Stream = a source of many values that arrive one after another, over time.
- A Future gives one value later. A Stream gives many values later.
A Stream sends three kinds of events:
- data event: a value
- error event: a problem
- done event: "no more values"
- You listen to a Stream to react to each event.
Simple lesson: a Future is one parcel, and a Stream is a conveyor belt of parcels.
A Real-World Example
Think of the school's live cricket score board on Sports Day.
- The score does not come all at once. It changes ball by ball.
- Each new score is a data event.
- A rain stop is an error event.
- "Match over" is the done event.
- You keep watching the board until the match is over. That is listening.
Every Run box on this page runs on the course's Dart runner in your browser.
This lesson builds on Async Basics and Futures. Make sure async, await and the event loop feel familiar.
What You Will Learn
In this lesson, you will learn:
- How to create Streams with
fromIterable,valueandperiodic. - How to listen with
listenand withawait for. - How to make your own Streams with
async*andStreamController. - How to change Streams with
map,where,takeand more. - Single-subscription and broadcast Streams.
Future vs Stream
| Future | Stream | |
|---|---|---|
| How many values? | one | zero, one or many |
| Ends with | the value, or an error | a done event |
| Read with | await or then | await for or listen |
| Example | one download | a timer ticking, chat messages |
Creating Streams
Ready-Made Streams
| Code | Gives |
|---|---|
Stream.fromIterable([1, 2, 3]) | each item of the list, one by one, then done |
Stream.value(5) | one value, then done |
Stream.periodic(Duration(seconds: 1), (i) => i) | 0, 1, 2 ... one per second, forever |
A stream of roll calls
void main() {
var names = Stream.fromIterable(['Asha', 'Ravi', 'Priya']);
names.listen((name) => print('$name: present'));
print('Roll call started');
}
Roll call started prints first. The Stream's events come later, from the event loop.
Ticking with periodic
A countdown that ticks every second
Future<void> main() async {
var ticks = Stream.periodic(Duration(seconds: 1), (i) => 3 - i).take(3);
await for (var t in ticks) {
print('$t...');
}
print('Go!');
}
periodicnever stops by itself.take(3)keeps only the first 3 values, then sends done.
Simple lesson: use fromIterable for known values, and periodic for values that come with time.
Listening to Streams
listen
listen = register functions for each kind of event:
stream.listen(
(value) { ... }, // data
onError: (e) { ... }, // error
onDone: () { ... }, // done
);
Data, error and done
Stream<int> scores() async* {
yield 4;
yield 6;
throw StateError('Rain stopped play');
}
void main() {
scores().listen(
(runs) => print('Runs: $runs'),
onError: (e) => print('Problem: ${e.message}'),
onDone: () => print('Match over'),
);
}
StreamSubscription and cancel
listen returns a StreamSubscription. Use it to stop listening:
Stop listening after the third tick
import 'dart:async';
void main() {
late StreamSubscription<int> sub;
sub = Stream.periodic(Duration(seconds: 1), (i) => i + 1).listen((n) {
print('Tick $n');
if (n == 3) {
sub.cancel();
print('Cancelled');
}
});
}
await for
await for = a loop over a Stream inside an async function. Each turn waits for the next value. The loop ends on done.
await for waits for each value
await for on a Stream that never ends (like periodic without take) never finishes. The code after the loop never runs.
Simple lesson: use listen to react to events, and await for to loop over values in order.
Making Your Own Streams
async* and yield
Generator = a function that produces values one by one.
async*marks a function that returns aStream.yield valuesends one value to the Stream.yield* otherStreamsends every value of another Stream.awaitworks inside too.
A generator with waiting and yield*
Stream<String> morning() async* {
yield 'Bell rings';
await Future.delayed(Duration(seconds: 1));
yield 'Prayer';
}
Stream<String> schoolDay() async* {
yield* morning();
yield 'Classes';
yield 'Home time';
}
Future<void> main() async {
await for (var event in schoolDay()) {
print(event);
}
}
StreamController
StreamController = a Stream you push values into by hand, from anywhere in your code.
controller.stream= the Stream others listen to.controller.add(value)= send a data event.controller.addError(e)= send an error event.controller.close()= send done.
A notice board you post to
import 'dart:async';
void main() {
var board = StreamController<String>();
board.stream.listen(
(notice) => print('NOTICE: $notice'),
onDone: () => print('Board closed for today'),
);
board.add('Sports Day on Friday');
board.add('Library closed at 3 pm');
board.close();
}
Simple lesson: write async* with yield for a Stream of steps, and use a StreamController to push values when things happen.
Transforming Streams
Many list methods (from Working with Iterables) work on Streams too. Each gives a new Stream.
| Method | Does |
|---|---|
map(f) | changes each value |
where(test) | keeps only values that pass |
expand(f) | turns each value into many |
take(n) / skip(n) | keeps the first n / skips the first n |
handleError(f) | catches error events, so the Stream continues |
Turn steps on and off, and send the events:
Only even marks, times ten
Future<void> main() async {
var marks = Stream.fromIterable([3, 8, 5, 10, 7, 4]);
var result = marks.where((m) => m.isEven).map((m) => m * 10).skip(1);
await for (var r in result) {
print(r);
}
}
Results That Are Futures
Some Stream methods wait for the whole Stream and give one answer, as a Future:
first, length, reduce and fold
Stream<int> runs() => Stream.fromIterable([4, 1, 6, 0, 2]);
Future<void> main() async {
print(await runs().first);
print(await runs().last);
print(await runs().length);
print(await runs().isEmpty);
print(await runs().reduce((a, b) => a + b));
print(await runs().fold(100, (total, r) => total + r));
print(await runs().toList());
}
runs() makes a fresh Stream each time, because a normal Stream can be listened to only once (next section).
Simple lesson: chain where, map and friends to shape a Stream, and use first, fold or toList to boil it down to one answer.
Single-Subscription and Broadcast
- Single-subscription Stream (the default) = one listener only. It keeps values until someone listens.
- Broadcast Stream = many listeners at once. Values sent with nobody listening are lost.
Listening twice to a normal stream
void main() {
var s = Stream.fromIterable([1, 2, 3]);
s.listen((v) => print('A got $v'));
s.listen((v) => print('B got $v'));
}
A broadcast stream for many listeners
import 'dart:async';
void main() {
var announcements = StreamController<String>.broadcast();
announcements.stream.listen((m) => print('Class 7A hears: $m'));
announcements.stream.listen((m) => print('Class 7B hears: $m'));
announcements.add('Assembly in 5 minutes');
announcements.close();
}
| Use | Kind |
|---|---|
| reading a file, one download | single-subscription |
| button taps, announcements, live scores | broadcast |
Easy way to remember
- Future = one parcel. Stream = a conveyor belt.
- listen = watch the belt. await for = take parcels one by one.
- async + yield* = put parcels on your own belt.
- broadcast = many people may watch the same belt.
Simple lesson: a normal Stream has one listener, and a broadcast Stream can have many.
TL;DR
- Stream = many values over time: data, error and done events.
- Create:
Stream.fromIterable,Stream.value,Stream.periodic,async*withyield,StreamController. - Read:
listen(withonError:andonDone:, returns a subscription you cancancel()), orawait for. - Transform:
map,where,expand,take,skip,handleError. Boil down:first,length,reduce,fold. - Single-subscription = one listener. Broadcast = many.
Common mistakes
| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| Listening twice to a normal Stream | Any Stream allows many listeners | Use .broadcast() or a new Stream | Bad state: Stream has already been listened to. |
await for on a never-ending Stream | The loop stops by itself | Use take, or cancel the subscription | Code after the loop never runs. |
Forgetting close() on a controller | Streams end by themselves | close() sends done | Listeners wait forever for done. |
yield in a normal function | Any function can yield | Mark it async* (or sync*) | A compile error. |
No onError: | Streams cannot fail | Handle errors with onError: or handleError | An unhandled exception. |
Check Yourself
Exercises
What to Remember
- A Stream gives many values over time, then a done event.
- Read it with
listen(withonError:andonDone:) or withawait for. - Make your own with
async*andyield, or push values with aStreamController. map,where,takeand friends make new Streams, andfoldortoListgive one answer.- A normal Stream allows one listener, and a broadcast Stream allows many.
Next: Isolates. You will run heavy work in the background without freezing your app.