Skip to content

Beginner · Async Dart and packages · topic 10 of 50 · 30 min

Streams

Explanation

A Future gives one value later; a Stream gives many values over time, like live scores or chat messages. Listen with listen() or await for. Create streams with async* functions and yield, or with a StreamController, which can be single-subscription or broadcast. Always cancel subscriptions and close controllers you create. Flutter shows streams with StreamBuilder, and Firestore delivers live data as streams.

Many values over time

Some data does not arrive once; it keeps coming: the score of a cricket match, a delivery rider's location, new chat messages, the user typing in a search box. A Stream is a sequence of asynchronous events. It can deliver values, errors, and finally a "done" signal.

Think of it like this: A Future is like ordering one parcel: it arrives once. A Stream is like a newspaper subscription: a new paper arrives every morning until you cancel.

Streams are everywhere in apps even when you do not create them yourself: a text field's changes, a sensor's readings, a database query that updates when rows change, or messages from a WebSocket. Learning to read and write stream code makes all of these feel the same.

Like a Future, a Stream can fail. An error event does not necessarily end the stream; it depends on the source. Always provide error handling with onError in listen, or a try/catch around await for, so one bad event does not crash the screen.

Creating a stream with async*

dart
Stream<int> countdown(int from) async* {
  for (var i = from; i >= 1; i--) {
    yield i;                                           // send a value
    await Future.delayed(const Duration(seconds: 1));
  }
}

Future<void> main() async {
  await for (final s in countdown(3)) {
    print('OTP expires in $s s');
  }
  print('OTP expired');
}
async* marks a function that returns a Stream; yield emits each value; await for loops over the values.

listen and subscriptions

dart
final sub = countdown(5).listen(
  (value) => print('tick $value'),
  onError: (e) => print('error: $e'),
  onDone: () => print('done'),
);
// later, for example when the screen closes:
// sub.cancel();
listen returns a StreamSubscription. Cancel it when you no longer need the values, or it keeps running.

StreamController

A StreamController lets you push values into a stream from ordinary code, for example when a WebSocket message arrives. By default its stream is single-subscription: only one listener, and values are buffered until someone listens. A broadcast stream allows many listeners but does not buffer for late ones.

dart
import 'dart:async';

class ScoreBoard {
  final _controller = StreamController<String>.broadcast();
  Stream<String> get scores => _controller.stream;

  void update(String score) => _controller.add(score);
  void dispose() => _controller.close();     // always close what you create
}

void main() {
  final board = ScoreBoard();
  board.scores.listen((s) => print('App: $s'));
  board.scores.listen((s) => print('Widget: $s'));
  board.update('IND 245/3 (40)');
  board.dispose();
}
Expose only the stream, keep the controller private, and close it in dispose.

Transforming streams

dart
final evensDoubled = Stream.fromIterable([1, 2, 3, 4])
    .where((n) => n.isEven)
    .map((n) => n * 2);
evensDoubled.listen(print);      // 4, then 8
Streams have where, map, take, distinct and more, like lists, but applied as values arrive.

How Flutter uses streams

Flutter's StreamBuilder widget listens to a stream and rebuilds part of the screen with the latest value. Cloud Firestore's snapshots() returns a stream, so a chat screen updates the moment a new message is written. State management libraries such as Bloc are also built on streams (module 7).

Common mistakes

  • Forgetting to cancel subscriptions in a widget's dispose: memory leaks and work on screens that are gone.
  • Not closing StreamControllers you create.
  • Listening twice to a single-subscription stream: "Bad state: Stream has already been listened to". Use a broadcast stream or a single listener.
  • Using a Stream when only one value is expected. Use a Future.

Need help?

Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.

Try:

Watch the lesson

Streams · 1/6

Future = one parcel Stream = subscription

A Future gives one value. A Stream gives many values over time.

Prefer a video by a teacher? Find free videos on “dart streams async* yield streamcontroller broadcast tutorial” on YouTube ↗

In real life: Live cricket score widget

A sports news app shows a small live score card on its home screen. The score arrives over the network as a stream of updates. A StreamBuilder shows "Loading..." until the first value, then the latest score, and the subscription is cancelled automatically when the card leaves the screen.

dart
Stream<String> liveScore() async* {
  const updates = ['IND 0/0', 'IND 45/1 (8)', 'IND 112/2 (18)'];
  for (final u in updates) {
    await Future.delayed(const Duration(seconds: 2));
    yield u;
  }
}

// In a Flutter widget (module 4 onward):
// StreamBuilder<String>(
//   stream: liveScore(),
//   builder: (context, snap) => Text(snap.data ?? 'Loading...'),
// )
Create the stream once (for example in initState), not inside build.

Practice (0/3 done)

Task 1 · Question

What does this print? Stream<int> nums() async* { yield 1; yield 2; yield 3; } Future<void> main() async { var total = 0; await for (final n in nums()) total += n; print(total); }

Task 2 · Question

You get "Bad state: Stream has already been listened to". Why?

Task 3 · Practice

Write a class OrderTracker with a private broadcast StreamController<String>, a public stream getter, a method update(status) and dispose(). In main, add two listeners, push "Placed", "Cooking" and "Out for delivery", then dispose. Also write an async* function that yields the numbers 10 to 1 with a 100 ms delay and sum them with await for.

Quick quiz

Question 1

1.Which keyword emits a value from an async* function?

Question 2

2.What does listen() return?

Question 3

3.Which stream type supports several listeners?

Question 4

4.Which Flutter widget rebuilds with the latest value of a stream?

Read more (free)

Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.

Which keyword emits a value from an async* function?

Answer: yield

yield sends the next value.

What does listen() return?

Answer: A StreamSubscription you can cancel

Keep it to cancel later.

Which stream type supports several listeners?

Answer: Broadcast

Broadcast streams allow multiple listeners.

Which Flutter widget rebuilds with the latest value of a stream?

Answer: StreamBuilder

StreamBuilder listens and rebuilds.

Where does an error from an awaited Future appear?Related

Answer: At the await, where try/catch can handle it

It is rethrown at the await.

From Futures and async/await

Which keyword creates a compile-time constant?Related

Answer: const

const values are known at compile time.

From Interview questions: Dart

What happens if you call two terminal operations on the same stream?Related

Answer: IllegalStateException

Streams are single-use.

From Streams: filter, map and collect

async function f() { return 3; } What is f()?Related

Answer: A promise that fulfils with 3

async functions always return promises.

From async and await

Pass the quiz (60% or more) or mark this topic as done when you are ready.

Your progress is saved on this device only.

Next steps

  1. 2 more topics in “Async Dart and packages” before its exam: Isolates for heavy work, Packages and pub.
  2. Module 3 exam: Async Dart and packages · 13 questions · 25 min, without help. 4 topics of this module still to finish first.
  3. Then the next module, Flutter setup and your first app. 50 topics left in the course before the final exam.

← Previous: Futures and async/await