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:

  1. How to create Streams with fromIterable, value and periodic.
  2. How to listen with listen and with await for.
  3. How to make your own Streams with async* and StreamController.
  4. How to change Streams with map, where, take and more.
  5. Single-subscription and broadcast Streams.

Future vs Stream

FutureStream
How many values?onezero, one or many
Ends withthe value, or an errora done event
Read withawait or thenawait for or listen
Exampleone downloada timer ticking, chat messages

Creating Streams

Ready-Made Streams

CodeGives
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!');
}
  • periodic never 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 a Stream.
  • yield value sends one value to the Stream.
  • yield* otherStream sends every value of another Stream.
  • await works 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.

MethodDoes
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();
}
UseKind
reading a file, one downloadsingle-subscription
button taps, announcements, live scoresbroadcast

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

  1. Stream = many values over time: data, error and done events.
  2. Create: Stream.fromIterable, Stream.value, Stream.periodic, async* with yield, StreamController.
  3. Read: listen (with onError: and onDone:, returns a subscription you can cancel()), or await for.
  4. Transform: map, where, expand, take, skip, handleError. Boil down: first, length, reduce, fold.
  5. Single-subscription = one listener. Broadcast = many.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Listening twice to a normal StreamAny Stream allows many listenersUse .broadcast() or a new StreamBad state: Stream has already been listened to.
await for on a never-ending StreamThe loop stops by itselfUse take, or cancel the subscriptionCode after the loop never runs.
Forgetting close() on a controllerStreams end by themselvesclose() sends doneListeners wait forever for done.
yield in a normal functionAny function can yieldMark it async* (or sync*)A compile error.
No onError:Streams cannot failHandle errors with onError: or handleErrorAn unhandled exception.

Check Yourself

Exercises

What to Remember

  • A Stream gives many values over time, then a done event.
  • Read it with listen (with onError: and onDone:) or with await for.
  • Make your own with async* and yield, or push values with a StreamController.
  • map, where, take and friends make new Streams, and fold or toList give 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.