A broadcast controller is a special type of stream controller in Dart that lets many listeners subscribe to the same stream at once. Unlike a regular stream controller, which allows only one listener, a broadcast controller supports multiple listeners receiving the same events.
You use broadcast controllers when you want to send data or events to many parts of your app at the same time. For example, a game can send updates to multiple players or a chat app can broadcast messages to many users.
The key difference from a single-subscription controller is that a broadcast controller doesn’t close after one listener is done. It keeps working until you close it explicitly, allowing many listeners to join and leave freely.
Creating a Broadcast Controller
To create a broadcast controller, use the .broadcast()
constructor of StreamController
. This sets up a stream that supports multiple listeners.
Here’s a simple example that creates a broadcast controller for strings:
import 'dart:async';
void main() {
var controller = StreamController<String>.broadcast();
// Now you can add events and have multiple listeners
}
With this controller, you can add data anytime, and all listeners will receive the same events simultaneously.
Adding Data to a Broadcast Controller
You can send data into a broadcast stream using .add()
or .sink.add()
. Both work the same, but .sink.add()
is cleaner and often preferred when passing the controller around.
Here’s a simple example where we send game updates into the stream:
import 'dart:async';
void main() {
var controller = StreamController<String>.broadcast();
// Just to show something is listening (optional here)
controller.stream.listen((event) => print('Update: $event'));
// Adding data
controller.add('Game started');
controller.sink.add('Player joined');
controller.close();
}
Both controller.add()
and controller.sink.add()
push events into the stream. Use .sink
when you want to expose only the ability to send data, not listen.
Listening to Broadcast Streams
Broadcast streams allow multiple listeners to subscribe at the same time — perfect for cases like chat rooms, game updates, or notifications where many parts of your app care about the same events.
Here’s how you can set up two listeners:
import 'dart:async';
void main() {
var controller = StreamController<String>.broadcast();
// Listener 1
controller.stream.listen((event) => print('Listener 1: $event'));
// Listener 2
controller.stream.listen((event) => print('Listener 2: $event'));
// Send some data
controller.add('Match Started');
controller.add('Goal Scored');
controller.close();
}
Each listener receives every event from the moment they subscribe.
What happens if a listener subscribes late?
A late listener will not receive past events. It only gets the events sent after it starts listening. This is different from something like a Future
, which remembers the result. Broadcast streams do not keep history—they’re live and in-the-moment.
Handling Errors and Closing the Controller
Broadcast controllers, like regular stream controllers, let you send both data and errors to listeners. Just like any stream controller, it’s important to close them when you’re finished. This makes sure resources are cleaned up and tells all listeners that the stream is complete.
Here’s how to do it all: you can use .add()
to send data, .addError()
to send an error, and finally .close()
to end the stream. Each listener can respond to these events using onData
, onError
, and onDone
:
import 'dart:async';
void main() {
var controller = StreamController<String>.broadcast();
// Listener 1
controller.stream.listen(
(event) => print('Listener 1: $event'),
onError: (e) => print('Listener 1 Error: $e'),
onDone: () => print('Listener 1: Stream closed'),
);
// Listener 2
controller.stream.listen(
(event) => print('Listener 2: $event'),
onError: (e) => print('Listener 2 Error: $e'),
onDone: () => print('Listener 2: Stream closed'),
);
// Add some data
controller.add('Update 1');
// Send an error
controller.addError('Something went wrong');
// Add more data
controller.add('Final update');
// Close the controller
controller.close();
}
In this example, two listeners are watching the same broadcast stream. They both get every event sent by the controller. When we use addError()
, both listeners see the error and handle it with onError
. Then we send one last message and close the stream with close()
, which triggers onDone
for both.
Closing the controller tells the stream there are no more events coming. After it’s closed, trying to add more events will cause an error.
This setup is handy when many parts of your app need to listen to the same events—like game updates, user actions, or app messages.
Using .sink
for Cleaner Event Adding
In a StreamController
, including broadcast controllers, .sink
provides a clean and focused way to send events into the stream. Instead of calling .add()
or .close()
directly on the controller, you use the .sink
interface, which only exposes what you need: .add()
, .addError()
, and .close()
.
This makes your code more readable and helps separate the input side of the stream (sending events) from the output side (listening to events). It’s especially helpful when you pass the sink to another part of your code—now it can send events without full access to the controller.
Here’s a fun example using .sink
:
import 'dart:async';
void main() {
var controller = StreamController<String>.broadcast();
controller.stream.listen((msg) => print('Listener: $msg'));
controller.sink.add('Hello');
controller.sink.add('World');
controller.sink.close();
}
In this example, we use .sink.add()
to send messages and .sink.close()
to end the stream. It’s cleaner and clearly shows that we’re only pushing data in.
Practical Example: Chat Room with Broadcast Controller
Let’s create a simple chat room simulation using a broadcast controller. In this setup, multiple users (listeners) will receive the same messages as they are sent through the stream. This shows how broadcast streams work in real time.
Here’s the example:
import 'dart:async';
void main() {
var chatController = StreamController<String>.broadcast();
// User 1 joins
chatController.stream.listen((msg) => print('User 1: $msg'));
// User 2 joins
chatController.stream.listen((msg) => print('User 2: $msg'));
// Simulate messages being sent
chatController.sink.add('Hello everyone!');
chatController.sink.add('Welcome to the chat.');
chatController.sink.add('Let’s start!');
chatController.close();
}
In this example, both User 1 and User 2 are listening to the same broadcast stream. When a message is sent using .sink.add()
, both users receive it at the same time. This is a perfect model for real-time apps where updates need to reach multiple parts of your program—like in chat apps, live scores, or shared game states.
Timed Events with Broadcast Controller
You can use a broadcast controller with a Timer
to send out events at regular intervals. This is great for things like countdowns, clock ticks, or status updates in real-time apps.
Here’s an example of a simple countdown that all listeners will hear at the same time:
import 'dart:async';
void main() {
var controller = StreamController<int>.broadcast();
int count = 5;
// Two listeners hear the same countdown
controller.stream.listen((n) => print('Listener A: $n'));
controller.stream.listen((n) => print('Listener B: $n'));
Timer.periodic(Duration(seconds: 1), (timer) {
if (count == 0) {
controller.sink.add(0);
controller.close();
timer.cancel();
} else {
controller.sink.add(count--);
}
});
}
Each second, the timer sends a new countdown value through the controller. Both Listener A and Listener B get the update at the same time. When the countdown reaches zero, the stream is closed, and the timer stops.
This kind of setup is useful for live features in your app, like countdowns to an event, refreshing data, or updating UI widgets based on time.
Cleaning Up: Canceling Subscriptions
When you’re done listening to a broadcast stream — maybe after receiving a few events — it’s a good idea to cancel the subscription. This helps free up resources and avoid unwanted updates.
Here’s an example where one listener cancels after getting three messages:
import 'dart:async';
void main() {
var controller = StreamController<String>.broadcast();
// Listener 1 listens and then cancels after 3 events
int messageCount = 0;
late StreamSubscription sub1;
sub1 = controller.stream.listen((msg) {
print('Listener 1: $msg');
messageCount++;
if (messageCount == 3) {
sub1.cancel();
print('Listener 1 stopped listening.');
}
});
// Listener 2 stays active
controller.stream.listen((msg) => print('Listener 2: $msg'));
controller.add('Hello');
controller.add('How are you?');
controller.add('Ready?');
controller.add('Let’s go!');
controller.close();
}
In this code, Listener 1 stops listening after the third message using cancel()
. Listener 2 keeps going until the stream is closed.
This pattern is useful when you want to listen only for a certain amount of time or specific events — like the first response from a server, or a few initial values during setup.
Conclusion
Broadcast controllers let many listeners share the same stream of events easily. They are perfect when different parts of your app need to hear the same data at once — like updates, messages, or notifications.
Using broadcast controllers is simple and flexible. You can add data, handle errors, close the stream, and even control subscriptions. This makes them a powerful tool for working with Dart streams in real-world apps.