On this page23 sections
- Prerequisites
- Connection With Previous Lectures
- Learning Objectives
- 1. What Is a Broadcast Stream?
- 2. Creating a Broadcast Stream
- 3. Several Listeners
- 4. Checking the Kind: isBroadcast
- 5. Late Listeners Miss Earlier Events
- 6. A Single-Subscription Stream Waits for You
- 7. Joining Over Time
- 8. Cancelling One Listener
- 9. asBroadcastStream() on an Existing Stream
- 10. Single or Broadcast?
- 11. A Real-World Example
- Code Examples
- Common Beginner Mistakes
- Best Practices
- Mini Challenge
- Practice Questions
- Practice Answers
- Quick Revision
- Key Takeaways
- What's Next?
Lecture 94 — Broadcast Streams
| Category | 15 — Asynchronous Programming |
| Lecture | 094 of 140 |
| Estimated video length | 8 min |
| Target | Dart 3.12.2 |
| Level | Intermediate |
A normal stream allows only one listener — a second
listencrashes 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, andawait(Lectures 088–089)- streams,
listen, andawait for(Lecture 092) StreamController,add,close, andcontroller.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:
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:
import 'dart:async';
void main() async {
var controller = StreamController<String>.broadcast();
controller.stream.listen((message) => print('Got: $message'));
controller.add('Hello');
await controller.close();
}Got: Hello3. Several Listeners
Now the second listen that crashed in Lecture 093 is allowed:
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();
}A got 1
B got 1
A got 2
B got 2Each 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:
import 'dart:async';
void main() {
var single = StreamController<int>();
var broadcast = StreamController<int>.broadcast();
print(single.stream.isBroadcast);
print(broadcast.stream.isBroadcast);
}false
trueA 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:
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();
}Late listener got 2Event 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:
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();
}Got 1
Got 2A 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:
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();
}Ali hears Song 1
Ali hears Song 2
Zayan hears Song 2Zayan 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:
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();
}Ali: Class starts at 9
Sara: Class starts at 9
Sara: Bring your notebookAli 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:
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();
}false
true
A: 1
B: 1
A: 2
B: 2The 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 order | each event is a separate announcement |
| one part of the program consumes it | several parts react to the same event |
| missing the start would break it | joining late and missing old events is fine |
| e.g. reading a file, downloading data | e.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:
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}');
}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? trueOne 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
import 'dart:async';
void main() async {
var controller = StreamController<String>.broadcast();
controller.stream.listen(print);
controller.add('Ali');
controller.add('Sara');
await controller.close();
}Ali
SaraExample 2 — Two Listeners
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();
}First: 3
Second: 30Example 3 — isBroadcast
import 'dart:async';
void main() {
var stream = StreamController<int>.broadcast().stream;
print('Broadcast: ${stream.isBroadcast}');
}Broadcast: trueExample 4 — A Late Listener
import 'dart:async';
void main() async {
var controller = StreamController<String>.broadcast();
controller.add('missed');
controller.stream.listen(print);
controller.add('heard');
await controller.close();
}heardExample 5 — Every Listener Gets onDone
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();
}A finished
B finishedExample 6 — asBroadcastStream()
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();
}One: x
Two: x
One: y
Two: yExample 7 — Student Marks Announcements
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');
}Teacher sees 80
Teacher sees 75
Teacher sees 85
Total: 240Example 8 — Shopping Cart Updates
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');
}Items: 2, total: 650.0Example 9 — Bank Alerts with a Cancelled Listener
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();
}Email: Deposit 1000
App: Deposit 1000
App: Withdrawal 200Example 10 — Temperature Sensor with Two Displays
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();
}Celsius: 25.0
Fahrenheit: 77.0
Celsius: 30.0
Fahrenheit: 86.0Common Beginner Mistakes
1. Listening twice to a single-subscription stream
import 'dart:async';
void main() {
var controller = StreamController<String>();
controller.stream.listen(print);
// controller.stream.listen(print); // ✗
}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
import 'dart:async';
void main() {
var numbers = StreamController<int>().stream;
numbers.asBroadcastStream().listen(print);
// numbers.asBroadcastStream().listen(print); // ✗
}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
StreamSubscriptionof any listener you may want to stop. - Call
asBroadcastStream()once and store the result. - Check
isBroadcastwhen 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
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();
}Ali: Test on Monday
Sara: Test on Monday
Sara: Bring a pencil
Zayan: Bring a pencilHow 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
- Create a broadcast controller of
String, listen once, add'Hi', and close it. - Print
isBroadcastfor a plainStreamController<int>().streamand for a broadcast one. - Attach two listeners to a broadcast stream of
intand add7. PrintLeft: 7andRight: 7.
Application
-
What does this print?
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(); } -
Change Question 4 to use a plain
StreamController<int>(). What does it print now, and why? -
Turn the stream of a plain
StreamController<int>into a broadcast stream and let two listeners add the values10and20to two separate totals. Print both totals. -
A broadcast stream of marks has a listener that counts marks and another that prints them. Add
90and60, then printCount: 2.
Challenge
- A broadcast stream of prices has listeners
cheapandall. Add100.0, cancelcheap, then add200.0. Print what each listener received. - Explain why the result of
controller.stream.asBroadcastStream()must be stored in a variable if you want two listeners. - 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
import 'dart:async';
void main() async {
var controller = StreamController<String>.broadcast();
controller.stream.listen(print);
controller.add('Hi');
await controller.close();
}HiQuestion 2 — Solution
import 'dart:async';
void main() {
print(StreamController<int>().stream.isBroadcast);
print(StreamController<int>.broadcast().stream.isBroadcast);
}false
trueQuestion 3 — Solution
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();
}Left: 7
Right: 7Question 4 — Solution
31 and 2 were added before anyone listened, so the broadcast stream threw them away.
Question 5 — Solution
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();
}1
2
3A single-subscription stream keeps its events until its listener arrives.
Question 6 — Solution
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');
}First: 30, second: 30Question 7 — Solution
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');
}Mark: 90
Mark: 60
Count: 2Question 8 — Solution
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();
}cheap: 100.0
all: 100.0
all: 200.0Question 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
- How many listeners can a single-subscription stream have? A broadcast stream?
- How do you create a broadcast controller?
- What does
isBroadcastreturn for the stream of a plainStreamController? - What happens to an event added to a broadcast stream with no listeners?
- Which kind of stream keeps events until a listener arrives?
- How do you stop one listener without affecting the others?
- 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;addandclosework 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.
isBroadcasttells you which kind a stream is.cancel()on oneStreamSubscriptionstops 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.