Watch the video (8 min)
On this page23 sections
  1. Prerequisites
  2. Connection With Previous Lectures
  3. Learning Objectives
  4. 1. What Is a Broadcast Stream?
  5. 2. Creating a Broadcast Stream
  6. 3. Several Listeners
  7. 4. Checking the Kind: isBroadcast
  8. 5. Late Listeners Miss Earlier Events
  9. 6. A Single-Subscription Stream Waits for You
  10. 7. Joining Over Time
  11. 8. Cancelling One Listener
  12. 9. asBroadcastStream() on an Existing Stream
  13. 10. Single or Broadcast?
  14. 11. A Real-World Example
  15. Code Examples
  16. Common Beginner Mistakes
  17. Best Practices
  18. Mini Challenge
  19. Practice Questions
  20. Practice Answers
  21. Quick Revision
  22. Key Takeaways
  23. What's Next?

Lecture 94 — Broadcast Streams

Category15 — Asynchronous Programming
Lecture094 of 140
Estimated video length8 min
TargetDart 3.12.2
LevelIntermediate

A normal stream allows only one listener — a second listen crashes the program. A broadcast stream lets any number of listeners hear the same events at the same time.

Contents

Prerequisites · Connection · Objectives · Lesson · Code Examples · Common Mistakes · Best Practices · Mini Challenge · Practice Questions · Answers · Takeaways

Every output and error message in this lecture is real. Each one was produced by running the code shown on Dart 3.12.2, so you will recognize it when you meet it yourself.


Prerequisites

Students should already understand, from Lectures 088–093:

  • Future.delayed, async, and await (Lectures 088–089)
  • streams, listen, and await for (Lecture 092)
  • StreamController, add, close, and controller.stream (Lecture 093)
  • the error you get when a stream is listened to twice (Lecture 093)

Connection With Previous Lectures

In Lecture 093, a second listener on the same controller crashed the program:

dart
var controller = StreamController<String>();
controller.stream.listen(print);
controller.stream.listen(print); // Bad state: Stream has already been listened to.

That rule protects data like a file being read — it should go to one reader, in one piece. But many events are meant for everyone: a "payment received" event should update the balance on screen, send a notification, and be written to a log. We need a stream that more than one part of the program can listen to.


Learning Objectives

By the end of this lecture you will be able to:

  • Explain the difference between a single-subscription and a broadcast stream
  • Create a broadcast stream with StreamController.broadcast()
  • Attach several listeners to one stream
  • Check a stream's kind with isBroadcast
  • Predict which events a late listener misses
  • Cancel one listener while the others keep listening
  • Turn an existing stream into a broadcast stream with asBroadcastStream()
  • Choose the right kind of stream for a task

1. What Is a Broadcast Stream?

Dart has two kinds of streams. A single-subscription stream — the kind we have used so far — has exactly one listener. A broadcast stream can have any number of listeners, and each event goes to all of them.

Analogy — a single-subscription stream is a phone call: one person on the other end, and they hear everything from the start. A broadcast stream is a radio station: anyone can tune in, but if you switch on late, you miss what was already said.


2. Creating a Broadcast Stream

Add .broadcast to the controller's constructor. Everything else — add, close, controller.stream — works as in Lecture 093:

dart
import 'dart:async';

void main() async {
  var controller = StreamController<String>.broadcast();
  controller.stream.listen((message) => print('Got: $message'));
  controller.add('Hello');
  await controller.close();
}
output
Got: Hello

3. Several Listeners

Now the second listen that crashed in Lecture 093 is allowed:

dart
import 'dart:async';

void main() async {
  var controller = StreamController<int>.broadcast();
  controller.stream.listen((v) => print('A got $v'));
  controller.stream.listen((v) => print('B got $v'));
  controller.add(1);
  controller.add(2);
  await controller.close();
}
output
A got 1
B got 1
A got 2
B got 2

Each event is delivered to every listener, in the order they started listening, before the next event is sent.


4. Checking the Kind: isBroadcast

Every stream has an isBroadcast property that tells you which kind it is:

dart
import 'dart:async';

void main() {
  var single = StreamController<int>();
  var broadcast = StreamController<int>.broadcast();
  print(single.stream.isBroadcast);
  print(broadcast.stream.isBroadcast);
}
output
false
true

A plain StreamController always gives a single-subscription stream.


5. Late Listeners Miss Earlier Events

A broadcast stream does not remember events. An event goes only to the listeners that are there at the moment it is added:

dart
import 'dart:async';

void main() async {
  var controller = StreamController<int>.broadcast();
  controller.add(1);
  controller.stream.listen((v) => print('Late listener got $v'));
  controller.add(2);
  await controller.close();
}
output
Late listener got 2

Event 1 was added when nobody was listening, so it is simply gone — no error, no warning.


6. A Single-Subscription Stream Waits for You

Compare the same program with a plain controller:

dart
import 'dart:async';

void main() async {
  var controller = StreamController<int>();
  controller.add(1);
  controller.add(2);
  controller.stream.listen((v) => print('Got $v'));
  await controller.close();
}
output
Got 1
Got 2

A single-subscription stream holds on to its events until its one listener arrives. A broadcast stream sends them out immediately, to whoever is there.


7. Joining Over Time

Events reach listeners a moment after add, not instantly, so the timed examples below wait briefly between steps. Here Zayan tunes in one second after Ali:

dart
import 'dart:async';

void main() async {
  var radio = StreamController<String>.broadcast();
  radio.stream.listen((song) => print('Ali hears $song'));

  radio.add('Song 1');
  await Future.delayed(Duration(seconds: 1));

  radio.stream.listen((song) => print('Zayan hears $song'));
  radio.add('Song 2');
  await radio.close();
}
output
Ali hears Song 1
Ali hears Song 2
Zayan hears Song 2

Zayan hears only what is played after he joins.


8. Cancelling One Listener

listen returns a StreamSubscription. Calling cancel() on it stops that listener only. The others keep receiving events:

dart
import 'dart:async';

void main() async {
  var controller = StreamController<String>.broadcast();
  var ali = controller.stream.listen((m) => print('Ali: $m'));
  controller.stream.listen((m) => print('Sara: $m'));

  controller.add('Class starts at 9');
  await Future.delayed(Duration(milliseconds: 100));

  await ali.cancel();
  controller.add('Bring your notebook');
  await controller.close();
}
output
Ali: Class starts at 9
Sara: Class starts at 9
Sara: Bring your notebook

Ali left the group; Sara still gets the second message. On a broadcast stream you can also listen again later — a new listener simply starts from the next event.


9. asBroadcastStream() on an Existing Stream

Sometimes you are handed a single-subscription stream but need several listeners. asBroadcastStream() wraps it in a broadcast stream:

dart
import 'dart:async';

void main() {
  var controller = StreamController<int>();
  var numbers = controller.stream; // single-subscription
  var shared = numbers.asBroadcastStream();
  print(numbers.isBroadcast);
  print(shared.isBroadcast);
  shared.listen((n) => print('A: $n'));
  shared.listen((n) => print('B: $n'));
  controller.add(1);
  controller.add(2);
  controller.close();
}
output
false
true
A: 1
B: 1
A: 2
B: 2

The wrapper is the original stream's one listener, and it passes every event on to all of its own listeners. Call asBroadcastStream() once, keep the result in a variable, and listen to that variable — never to the original stream again.


10. Single or Broadcast?

Use a single-subscription stream when…Use a broadcast stream when…
the data must arrive complete and in ordereach event is a separate announcement
one part of the program consumes itseveral parts react to the same event
missing the start would break itjoining late and missing old events is fine
e.g. reading a file, downloading datae.g. button taps, notifications, price updates

A file read by two listeners, one of them late, would give the late one half a file. A button tap from a minute ago, on the other hand, is of no interest to a screen that opens now.


11. A Real-World Example

Sara's bank account announces every payment. The app screen, the SMS service, and an audit log all listen. Sara then turns off SMS alerts, and later a statement page opens:

dart
import 'dart:async';

void main() async {
  var payments = StreamController<double>.broadcast();
  var log = <double>[];

  payments.stream.listen((amount) => print('App: received $amount'));
  var sms = payments.stream.listen((amount) => print('SMS: received $amount'));
  payments.stream.listen((amount) => log.add(amount));

  payments.add(500.0);
  await Future.delayed(Duration(milliseconds: 100));

  await sms.cancel(); // Sara turns off SMS alerts
  payments.add(250.0);
  await Future.delayed(Duration(milliseconds: 100));

  payments.stream.listen((amount) => print('Statement: received $amount'));
  payments.add(100.0);
  await payments.close();

  print('Log: $log');
  print('Broadcast? ${payments.stream.isBroadcast}');
}
output
App: received 500.0
SMS: received 500.0
App: received 250.0
App: received 100.0
Statement: received 100.0
Log: [500.0, 250.0, 100.0]
Broadcast? true

One event source, four listeners joining and leaving at different times. The log saw every payment because it was there from the start; the statement page saw only the last one.


Code Examples

Example 1 — A Broadcast Controller

dart
import 'dart:async';

void main() async {
  var controller = StreamController<String>.broadcast();
  controller.stream.listen(print);
  controller.add('Ali');
  controller.add('Sara');
  await controller.close();
}
output
Ali
Sara

Example 2 — Two Listeners

dart
import 'dart:async';

void main() async {
  var controller = StreamController<int>.broadcast();
  controller.stream.listen((n) => print('First: $n'));
  controller.stream.listen((n) => print('Second: ${n * 10}'));
  controller.add(3);
  await controller.close();
}
output
First: 3
Second: 30

Example 3 — isBroadcast

dart
import 'dart:async';

void main() {
  var stream = StreamController<int>.broadcast().stream;
  print('Broadcast: ${stream.isBroadcast}');
}
output
Broadcast: true

Example 4 — A Late Listener

dart
import 'dart:async';

void main() async {
  var controller = StreamController<String>.broadcast();
  controller.add('missed');
  controller.stream.listen(print);
  controller.add('heard');
  await controller.close();
}
output
heard

Example 5 — Every Listener Gets onDone

dart
import 'dart:async';

void main() async {
  var controller = StreamController<int>.broadcast();
  controller.stream.listen((_) {}, onDone: () => print('A finished'));
  controller.stream.listen((_) {}, onDone: () => print('B finished'));
  await controller.close();
}
output
A finished
B finished

Example 6 — asBroadcastStream()

dart
import 'dart:async';

void main() {
  var controller = StreamController<String>();
  var shared = controller.stream.asBroadcastStream();
  shared.listen((s) => print('One: $s'));
  shared.listen((s) => print('Two: $s'));
  controller.add('x');
  controller.add('y');
  controller.close();
}
output
One: x
Two: x
One: y
Two: y

Example 7 — Student Marks Announcements

dart
import 'dart:async';

void main() async {
  var results = StreamController<int>.broadcast();
  var total = 0;
  results.stream.listen((mark) => print('Teacher sees $mark'));
  results.stream.listen((mark) => total += mark);
  results.add(80);
  results.add(75);
  results.add(85);
  await results.close();
  print('Total: $total');
}
output
Teacher sees 80
Teacher sees 75
Teacher sees 85
Total: 240

Example 8 — Shopping Cart Updates

dart
import 'dart:async';

void main() async {
  var cart = StreamController<double>.broadcast();
  var count = 0;
  var total = 0.0;
  cart.stream.listen((_) => count++);
  cart.stream.listen((price) => total += price);
  cart.add(500.0);
  cart.add(150.0);
  await cart.close();
  print('Items: $count, total: $total');
}
output
Items: 2, total: 650.0

Example 9 — Bank Alerts with a Cancelled Listener

dart
import 'dart:async';

void main() async {
  var alerts = StreamController<String>.broadcast();
  var email = alerts.stream.listen((a) => print('Email: $a'));
  alerts.stream.listen((a) => print('App: $a'));
  alerts.add('Deposit 1000');
  await Future.delayed(Duration(milliseconds: 100));
  await email.cancel();
  alerts.add('Withdrawal 200');
  await alerts.close();
}
output
Email: Deposit 1000
App: Deposit 1000
App: Withdrawal 200

Example 10 — Temperature Sensor with Two Displays

dart
import 'dart:async';

void main() async {
  var sensor = StreamController<double>.broadcast();
  sensor.stream.listen((c) => print('Celsius: $c'));
  sensor.stream.listen((c) => print('Fahrenheit: ${c * 9 / 5 + 32}'));
  sensor.add(25.0);
  sensor.add(30.0);
  await sensor.close();
}
output
Celsius: 25.0
Fahrenheit: 77.0
Celsius: 30.0
Fahrenheit: 86.0

Common Beginner Mistakes

1. Listening twice to a single-subscription stream

dart
import 'dart:async';

void main() {
  var controller = StreamController<String>();
  controller.stream.listen(print);
  // controller.stream.listen(print); // ✗
}
output
Unhandled exception:
Bad state: Stream has already been listened to.

Use StreamController<String>.broadcast() if several listeners are intended.

2. Calling asBroadcastStream() twice on the same stream

dart
import 'dart:async';

void main() {
  var numbers = StreamController<int>().stream;
  numbers.asBroadcastStream().listen(print);
  // numbers.asBroadcastStream().listen(print); // ✗
}
output
Unhandled exception:
Bad state: Stream has already been listened to.

Each call creates a new wrapper, and each wrapper listens to the original stream. Call it once and share the result.

3. Expecting a late listener to see old events

No error appears — the events are just missing. If a listener must see everything, attach it before the first add, or use a single-subscription stream.

4. Adding events before anyone listens

On a broadcast stream, add with no listeners throws the event away. Set up listeners first, then start adding.

5. Using a broadcast stream for file or download data

A late listener would receive only the end of the data. Data that must arrive complete belongs in a single-subscription stream.

6. Forgetting to cancel a listener that is no longer needed

A listener keeps receiving events until it is cancelled or the stream closes. Keep the StreamSubscription and call cancel() when that part of the program is finished.


Best Practices

  • Use broadcast streams for events many parts of a program care about; use single-subscription streams for data one consumer must receive completely.
  • Attach listeners before adding events you cannot afford to lose.
  • Keep the StreamSubscription of any listener you may want to stop.
  • Call asBroadcastStream() once and store the result.
  • Check isBroadcast when you are unsure which kind of stream a function gave you.
  • Close the controller when no more events will come, so every listener gets onDone.

Mini Challenge

A classroom announcement stream has two listeners: Ali and Sara. Send 'Test on Monday'. Then Ali cancels, Zayan joins, and you send 'Bring a pencil'. Print what each person hears.

Try it yourself before opening the solution.

Show solution
dart
import 'dart:async';

void main() async {
  var announcements = StreamController<String>.broadcast();
  var ali = announcements.stream.listen((a) => print('Ali: $a'));
  announcements.stream.listen((a) => print('Sara: $a'));

  announcements.add('Test on Monday');
  await Future.delayed(Duration(milliseconds: 100));

  await ali.cancel();
  announcements.stream.listen((a) => print('Zayan: $a'));
  announcements.add('Bring a pencil');
  await announcements.close();
}
output
Ali: Test on Monday
Sara: Test on Monday
Sara: Bring a pencil
Zayan: Bring a pencil

How it works — Ali hears only the first announcement because he cancels before the second. Zayan hears only the second because he joins after the first. Sara was there for both.


Practice Questions

Solve each one before looking at the answers.

Beginner

  1. Create a broadcast controller of String, listen once, add 'Hi', and close it.
  2. Print isBroadcast for a plain StreamController<int>().stream and for a broadcast one.
  3. Attach two listeners to a broadcast stream of int and add 7. Print Left: 7 and Right: 7.

Application

  1. What does this print?

    dart
    import 'dart:async';
    
    void main() async {
      var c = StreamController<int>.broadcast();
      c.add(1);
      c.add(2);
      c.stream.listen(print);
      c.add(3);
      await c.close();
    }
  2. Change Question 4 to use a plain StreamController<int>(). What does it print now, and why?

  3. Turn the stream of a plain StreamController<int> into a broadcast stream and let two listeners add the values 10 and 20 to two separate totals. Print both totals.

  4. A broadcast stream of marks has a listener that counts marks and another that prints them. Add 90 and 60, then print Count: 2.

Challenge

  1. A broadcast stream of prices has listeners cheap and all. Add 100.0, cancel cheap, then add 200.0. Print what each listener received.
  2. Explain why the result of controller.stream.asBroadcastStream() must be stored in a variable if you want two listeners.
  3. For each, choose single-subscription or broadcast and give one reason: (a) reading a report file, (b) "new message" events in a chat app, (c) downloading an image.

Practice Answers

Question 1 — Solution

dart
import 'dart:async';

void main() async {
  var controller = StreamController<String>.broadcast();
  controller.stream.listen(print);
  controller.add('Hi');
  await controller.close();
}
output
Hi

Question 2 — Solution

dart
import 'dart:async';

void main() {
  print(StreamController<int>().stream.isBroadcast);
  print(StreamController<int>.broadcast().stream.isBroadcast);
}
output
false
true

Question 3 — Solution

dart
import 'dart:async';

void main() async {
  var controller = StreamController<int>.broadcast();
  controller.stream.listen((n) => print('Left: $n'));
  controller.stream.listen((n) => print('Right: $n'));
  controller.add(7);
  await controller.close();
}
output
Left: 7
Right: 7

Question 4 — Solution

output
3

1 and 2 were added before anyone listened, so the broadcast stream threw them away.

Question 5 — Solution

dart
import 'dart:async';

void main() async {
  var c = StreamController<int>();
  c.add(1);
  c.add(2);
  c.stream.listen(print);
  c.add(3);
  await c.close();
}
output
1
2
3

A single-subscription stream keeps its events until its listener arrives.

Question 6 — Solution

dart
import 'dart:async';

void main() async {
  var controller = StreamController<int>();
  var shared = controller.stream.asBroadcastStream();
  var first = 0;
  var second = 0;
  shared.listen((n) => first += n);
  shared.listen((n) => second += n);
  controller.add(10);
  controller.add(20);
  await controller.close();
  print('First: $first, second: $second');
}
output
First: 30, second: 30

Question 7 — Solution

dart
import 'dart:async';

void main() async {
  var marks = StreamController<int>.broadcast();
  var count = 0;
  marks.stream.listen((_) => count++);
  marks.stream.listen((m) => print('Mark: $m'));
  marks.add(90);
  marks.add(60);
  await marks.close();
  print('Count: $count');
}
output
Mark: 90
Mark: 60
Count: 2

Question 8 — Solution

dart
import 'dart:async';

void main() async {
  var prices = StreamController<double>.broadcast();
  var cheap = prices.stream.listen((p) => print('cheap: $p'));
  prices.stream.listen((p) => print('all: $p'));
  prices.add(100.0);
  await Future.delayed(Duration(milliseconds: 100));
  await cheap.cancel();
  prices.add(200.0);
  await prices.close();
}
output
cheap: 100.0
all: 100.0
all: 200.0

Question 9 — Solution

Each asBroadcastStream() call creates a new wrapper that listens to the original single-subscription stream. A second call would be a second listener on it and fail with Bad state: Stream has already been listened to. Storing the wrapper once lets both listeners share it.

Question 10 — Solution

(a) Single-subscription — the file must be read completely and in order by one reader. (b) Broadcast — the message list, the notification badge, and the sound can all react, and an old event does not matter to a screen opened later. (c) Single-subscription — a late listener would get only part of the image.


Quick Revision

  1. How many listeners can a single-subscription stream have? A broadcast stream?
  2. How do you create a broadcast controller?
  3. What does isBroadcast return for the stream of a plain StreamController?
  4. What happens to an event added to a broadcast stream with no listeners?
  5. Which kind of stream keeps events until a listener arrives?
  6. How do you stop one listener without affecting the others?
  7. What does asBroadcastStream() do, and why call it only once?

Key Takeaways

  • A single-subscription stream has one listener; a broadcast stream can have many.
  • StreamController<T>.broadcast() creates a broadcast stream; add and close work as before.
  • Every listener receives every event added while it is listening.
  • Broadcast streams do not remember events: late listeners miss them, and events with no listener are lost.
  • isBroadcast tells you which kind a stream is.
  • cancel() on one StreamSubscription stops only that listener.
  • asBroadcastStream() shares an existing stream — call it once and store the result.
  • Use single-subscription for complete data (files, downloads) and broadcast for events (taps, notifications, updates).

What's Next?

Lecture 095 — Stream Transformations. So far every listener received the raw events and did all the work itself — converting Celsius, skipping small payments, counting. Next we will learn how to transform a stream itself, so listeners receive exactly the events they need.


Video lecture

Subscribe
Placeholder video — the real recording isn't linked yet.