Streams: Sequences of Asynchronous Events
In the last chapter you met the Future, which gives you one value later. A Stream is different: it gives you many values over time. Think of a Future as a single letter arriving in the mail. A Stream is more like a subscription to a magazine — new issues keep arriving, one after another.
Streams show up whenever data arrives over time: messages in a chat, clicks from a button, or periodic timer ticks.
The easiest way to read a Stream is with await for, which runs a loop that waits for each value:
Future<void> main() async {
final ticks = Stream.periodic(Duration(milliseconds: 100), (i) => i);
await for (final i in ticks.take(3)) {
print('tick $i');
}
}Here, Stream.periodic creates a Stream that emits a new number every 100 milliseconds. The .take(3) method means "only listen for three values, then stop." Without it, the loop would run forever.
You can also transform Streams much like lists. If list.map(...) changes every item in a list, stream.map(...) changes every item as it arrives:
final doubled = ticks.map((i) => i * 2);
await for (final v in doubled.take(3)) {
print(v); // 0, 2, 4
}Isolates: Dart's Model of Concurrency
Sometimes you need to do heavy work, like reading a huge file or calculating something expensive. If you do that work while your program is busy handling events, everything else gets stuck.
Dart solves this with isolates. An isolate is a small, independent worker with its own memory. Each isolate runs its code one thing at a time, and isolates never share memory with each other. Instead, they pass messages back and forth.
This design means you never get the classic bugs that come from two threads changing the same data at once — because in Dart, they never touch the same data.
You rarely write isolate code by hand. A simple helper is compute-style spawning, or you can use a SendPort/ReceivePort pair to send messages:
import 'dart:isolate';
int heavySum(int n) => List.generate(n, (i) => i).reduce((a, b) => a + b);
Future<void> main() async {
final result = await Isolate.run(() => heavySum(1000000));
print(result);
}Isolate.run spins up a worker, runs your function there, sends back the answer, and shuts the worker down. Your main isolate stays free the whole time.
Exercise
Which would you use to receive chat messages one by one as they arrive: a Future, a Stream, or an isolate?
Show answer
A Stream. It delivers many values over time, which is exactly what a chat feed needs. A Future gives only one value, and an isolate is for running work separately, not receiving a sequence of events.
Exercise
Why do isolates not share memory?
Show answer
Sharing memory between simultaneous workers causes hard-to-find bugs when they modify the same data. Dart avoids the whole class of problems by giving each isolate its own memory and letting them communicate only through messages.
Spotted a mistake, or code that does not work?