Dart: Multi-Subscription Streams

A multi-subscription stream lets many listeners receive the same events from one source. Unlike single-subscription streams, where only one listener can subscribe, multi-subscription streams allow several parts of your app to listen at the same time.

You use multi-subscription streams when you want to share data or events with multiple listeners. For example, in a chat app, many users might listen to messages coming from the same stream. This way, everyone gets updates without needing separate streams for each listener.

Using StreamController.broadcast()

A broadcast controller lets you create a stream that supports multiple listeners. Many listeners can subscribe and receive the same events in real time.

Here’s how to create one and add events:

import 'dart:async';

void main() {

  var controller = StreamController<String>.broadcast();

  // Listener 1 subscribes to the stream
  controller.stream.listen((event) => print('Listener 1: $event'));

  // Listener 2 subscribes to the same stream
  controller.stream.listen((event) => print('Listener 2: $event'));

  // Add events to the stream
  controller.add('Start');
  controller.add('Play');

  // Close the stream when done
  controller.close();

}

In this example, both Listener 1 and Listener 2 receive the 'Start' and 'Play' events because the stream is broadcast. When the stream closes, both listeners stop receiving events.

Broadcast streams are great when you want to share the same data with many listeners at the same time.

Listening to a Broadcast Stream

Broadcast streams allow multiple listeners to get the same events as they happen. Each listener starts receiving events only from the moment they subscribe.

Here’s an example showing two listeners — one subscribes early, the other joins later:

import 'dart:async';

void main() async {

  var controller = StreamController<String>.broadcast();

  // First listener subscribes immediately
  controller.stream.listen((event) => print('Listener 1: $event'));

  controller.add('Hello');
  controller.add('Welcome');

  // Second listener subscribes after some events
  await Future.delayed(Duration(seconds: 1), () {

    controller.stream.listen((event) => print('Listener 2: $event'));

  });

  controller.add('Ready?');
  controller.add('Go!');

  controller.close();

}

In this example:

  • Listener 1 receives all events: 'Hello', 'Welcome', 'Ready?', and 'Go!'.
  • Listener 2 subscribes late and only gets events from 'Ready?' and 'Go!'.

Broadcast streams don’t replay past events to new listeners. They only send events happening after the listener joins.

Creating a Multi-Subscription Stream with Stream.multi()

Stream.multi() lets you create a multi-subscription stream by providing a callback that controls the events for each listener individually. The callback gets a special controller to add events, manage timing, and close the stream.

Here’s an example of a countdown stream that sends numbers 3, 2, 1 to each listener separately:

import 'dart:async';

Stream<int> countdownStream() => Stream.multi((controller) {

  int count = 3;

  Timer.periodic(Duration(seconds: 1), (timer) {

    if (count == 0) {

      controller.close();
      timer.cancel();

    } else {
      controller.add(count--);
    }

  });

});

void main() {

  var stream = countdownStream();

  stream.listen((num) => print('Listener 1: $num'));

  Future.delayed(Duration(seconds: 2), () {
    stream.listen((num) => print('Listener 2: $num'));
  });

}

Each time a listener subscribes, the callback runs fresh, starting a new countdown just for that listener. This means Listener 1 begins receiving the countdown immediately, printing 3, 2, 1.

Listener 2 subscribes after a delay of 2 seconds. Because the stream generates events separately for each listener, Listener 2 starts its own countdown from 3, 2, 1 — it does not join Listener 1’s stream in the middle.

This behavior differs from a broadcast stream where all listeners share the same event source. Stream.multi() is useful when every listener needs to get its own independent set of events, rather than sharing the same stream of data.

Key Difference: Adding Events Later

With StreamController.broadcast(), you have full control to add events at any time using the controller’s .add() method. This means you can push data into the stream whenever you want, and all listeners will receive those events.

In contrast, Stream.multi() requires you to add events only inside the callback function that you provide when creating the stream. This callback runs separately for each listener, so the events are generated on demand, and you cannot add new events later from outside the callback.

This means with Stream.multi(), the stream is more like a factory creating a fresh sequence of events for each subscriber. While with a broadcast controller, you have a shared event source you can update at any time.

When to Use Which

Use a broadcast controller when you want manual, ongoing control over the stream. It lets you add events anytime from anywhere in your code, making it great for shared sources like user input, sensor data, or game events that happen over time.

Choose Stream.multi() when you want a simple way to generate events fresh for each listener. It works well for streams where each subscriber should get its own independent sequence, like countdowns, animations, or data that is created on demand.

Fun Example: Chat Room with Broadcast Controller

Imagine a simple chat room where multiple users listen to messages and can send their own. A broadcast controller lets everyone hear every message in real-time.

import 'dart:async';

void main() {

  var chatController = StreamController<String>.broadcast();

  // User 1 listens to messages
  chatController.stream.listen((msg) => print('User 1 received: $msg'));

  // User 2 listens to messages
  chatController.stream.listen((msg) => print('User 2 received: $msg'));

  // Users send messages
  chatController.add('User 1: Hello!');
  chatController.add('User 2: Hi there!');

  chatController.close();

}

In this example, both User 1 and User 2 receive all messages sent through the broadcast controller. This setup works perfectly for chat apps where everyone needs to get the same updates at the same time.

Fun Example: Custom Notification Stream with Stream.multi()

With Stream.multi(), each listener gets a fresh set of notifications generated just for them. This is perfect when you want independent event flows per subscriber.

import 'dart:async';

Stream<String> notificationStream() => Stream.multi((controller) {

  List<String> notifications = ['Welcome!', 'New message', 'Update available'];

  // Send notifications one by one with a delay for each listener
  Future.forEach(notifications, (String note) async {

    await Future.delayed(Duration(seconds: 1));
    controller.add(note);

  }).then((_) => controller.close());

});

void main() {

  var stream = notificationStream();

  // First listener subscribes immediately
  stream.listen((note) => print('Listener 1 got: $note'));

  // Second listener subscribes after 2 seconds, gets full fresh notifications
  Future.delayed(Duration(seconds: 2), () {
    stream.listen((note) => print('Listener 2 got: $note'));
  });

}

Each listener receives the full list of notifications, one at a time, with delays. The notifications start fresh for each listener, making Stream.multi() great for personalized or independent event sequences.

Conclusion

Multi-subscription streams let multiple listeners receive the same stream of events. You can create them using two main ways: StreamController.broadcast() and Stream.multi().

With StreamController.broadcast(), you get full control—you can add events anytime, and all listeners share those events. This works well when you want to push events manually and continuously.

On the other hand, Stream.multi() creates a fresh event sequence for each listener inside its callback. It’s simpler and perfect when each listener should get their own independent flow of events.

Choose the method that fits your needs: use broadcast controllers for manual, ongoing event control, and Stream.multi() for clean, per-listener event generation. Both are powerful tools for handling multiple listeners in Dart streams.