Streams

Dart · Advanced · 14 min

Streams model repeated async events such as user input, sockets, database watches and Flutter's StreamBuilder.

A `Stream<T>` delivers a sequence of asynchronous events — data, errors, and a final done. Where a Future gives one value, a stream gives many.

Consuming:

  • await for (final event in stream) { ... } inside an async function, with try/catch for errors. The loop ends when the stream closes.
  • stream.listen(onData, onError: ..., onDone: ...) returns a StreamSubscription you can pause, resume and must cancel() when done (for example in a Flutter dispose).

Streams have iterable-like transformers: map, where, take, skip, distinct, asyncMap, expand, plus first, toList() and fold that return futures.

Producing:

  • An **async* generator** function with yield (and yield* to forward another stream). It's lazy: the body starts when someone listens and pauses when the listener pauses.
  • A `StreamController` when events come from elsewhere: controller.add(x), addError, close(), and expose controller.stream.

Single-subscription streams (the default, e.g. file reads) allow exactly one listener and buffer events until it subscribes. Broadcast streams (StreamController.broadcast(), asBroadcastStream()) allow many listeners but drop events no one is listening to. Listening twice to a single-subscription stream throws a StateError.

For advanced operators (debounce, combineLatest), the rxdart and stream_transform packages are common.

Example

import 'dart:async';
Stream<int> countdown(int from) async* {
  for (var i = from; i > 0; i--) {
    await Future.delayed(const Duration(milliseconds: 10));
    yield i;
  }
}

Future<void> main() async {
  await for (final n in countdown(3)) {
    print(n);                                    // 3 2 1
  }

  final evens = countdown(6).where((n) => n.isEven).map((n) => n * 10);
  print(await evens.toList());                   // [60, 40, 20]

  final clicks = StreamController<String>.broadcast();
  final sub = clicks.stream.listen((c) => print('A got $c'));
  clicks.stream.where((c) => c == 'ok').listen((_) => print('B saw ok'));
  clicks..add('ok')..add('cancel');
  await Future<void>.delayed(Duration.zero);     // let events deliver
  await sub.cancel();
  await clicks.close();
  // A got ok / B saw ok / A got cancel
}

Remember

  • Consume with await for or listen, and cancel subscriptions
  • Produce with async*/yield or a StreamController
  • Single-subscription = one listener; broadcast = many, no buffering

Watch out for

  • Never cancelling a listen subscription, leaking memory and work after a widget is gone.

Check yourself

  1. What happens if you call listen twice on a default (single-subscription) StreamController's stream?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: dart.dev/libraries/async/using-streams