| |

Dart 41 🎯 Single-Subscription vs Broadcast Streams (StreamController)

A StreamController is the object that creates a stream and gives you the methods to add events to it. It is the most flexible way to create a stream, because it gives the producer full control over when values are emitted, when errors are added, and when the stream is closed. The controller has a stream property that exposes the stream to consumers, and it has add, addError, and close methods that the producer calls. The controller is the write end; the stream is the read end.

The most important decision when creating a StreamController is whether the stream should be single-subscription or broadcast. The default constructor StreamController() creates a single-subscription stream: only one listener can attach for the lifetime of the stream. The named constructor StreamController.broadcast() creates a broadcast stream: any number of listeners can attach, and events are delivered to all of them. The two varieties have different behavior, different use cases, and different failure modes, and choosing the wrong one is the source of most StreamController bugs.

The difference is not just about the number of listeners. It is about buffering. A single-subscription stream buffers events until a listener attaches. If the producer adds events before anyone is listening, the events are queued, and they are delivered in order when the listener attaches. A broadcast stream does not buffer. Events added before a listener attaches are dropped, and events added while no listener is attached are lost. This is the correct behavior for an event source that represents “what is happening now,” and the wrong behavior for a source that represents “a sequence that should be consumed from the beginning.”

This chapter covers three areas. First, why the two varieties exist — the buffering problem, the multi-listener problem, and the lifecycle callbacks. Second, how each variety works — the single-subscription controller, its buffering and pause behavior, and the broadcast controller, its no-buffering and multi-listener behavior. Third, how to choose between them and how to convert — the decision criteria, the asBroadcastStream() conversion, and the patterns for sharing a single-subscription stream among multiple consumers. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the two models.

Key point: StreamController() creates a single-subscription stream that buffers events until a listener attaches. StreamController.broadcast() creates a broadcast stream that does not buffer and delivers events to all current listeners. A single-subscription stream can be converted to a broadcast stream with asBroadcastStream(), but the conversion is one-way. Choose the variety based on whether the events represent a sequence to be consumed from the beginning or a stream of current events.


Why single-subscription and broadcast streams exist

The buffering problem. A single-subscription stream is designed for a sequence that must be consumed in order, from the beginning. If the producer adds events before the consumer is ready, the events must not be lost. The single-subscription controller buffers them. The events are queued in the controller’s internal buffer, and when the listener attaches, the buffered events are delivered first, followed by any events added after the listener attached. This is the correct behavior for a file read, an HTTP response body, or any source where the sequence is the point.

The multi-listener problem. A broadcast stream is designed for an event source that multiple consumers need to observe. A user authentication state, a WebSocket message stream, or a sensor reading is relevant to several parts of an application. A single-subscription stream allows only one listener, so sharing it requires either converting it to a broadcast stream or wrapping it in a mechanism that distributes the events. The broadcast controller is the direct solution: any number of listeners can attach, and each one receives every event added after it attached.

The no-buffering problem. A broadcast stream does not buffer. This is the consequence of allowing multiple listeners. If the stream buffered events, a new listener that attaches later would receive events that the previous listeners already consumed, which is usually not what is wanted. A broadcast stream represents “the current state of events,” and a listener that attaches late has missed the earlier events. This is the correct behavior for a state stream, where the current value is what matters, and the wrong behavior for a sequence that must be replayed from the beginning.

The pause problem. A single-subscription stream supports pause and resume. When the subscription is paused, the controller buffers events. When it is resumed, the buffered events are delivered. This gives the consumer control over the pace. A broadcast stream also supports pause, but the behavior is different: when a broadcast subscription is paused, events are buffered for that subscription up to a limit, and if the limit is exceeded, the subscription is canceled. The pause behavior is one of the reasons the two varieties are not interchangeable.

The lifecycle-callback problem. A StreamController accepts callbacks for the subscription lifecycle: onListen, onPause, onResume, and onCancel. For a single-subscription controller, these callbacks fire when the single listener attaches, pauses, resumes, or cancels. For a broadcast controller, they fire when the first listener attaches, when the last listener pauses, when the last listener resumes, and when the last listener cancels. The callbacks are the hook for starting and stopping the underlying resource: opening a socket when the first listener attaches, and closing it when the last listener cancels.

The trade-off. The single-subscription stream is simpler and safer: it buffers, it supports one listener, and the lifecycle is straightforward. The broadcast stream is more flexible but more dangerous: it drops events, it supports many listeners, and the lifecycle is more complex. The choice depends on what the events represent. A sequence to be consumed from the beginning is a single-subscription stream. A stream of current events that multiple consumers observe is a broadcast stream.


a. The single-subscription controller

The default constructor StreamController() creates a single-subscription stream. The stream property exposes the stream, and the add, addError, and close methods add events to it.

final controller = StreamController<int>();

controller.stream.listen(
  (value) => print('value: $value'),
  onError: (error) => print('error: $error'),
  onDone: () => print('done'),
);

controller.add(1);
controller.add(2);
controller.addError(Exception('boom'));
controller.add(3);
controller.close();

The output is:

value: 1
value: 2
error: Exception: boom
value: 3
done

The controller buffers the events until the listener attaches. In this example, the listener attaches before the events are added, so the events are delivered immediately. If the events were added before the listener attached, they would be buffered and delivered when the listener attaches.

Only one listener can attach to a single-subscription stream. A second call to listen throws a StateError:

final controller = StreamController<int>();

controller.stream.listen((v) => print('first: $v'));

controller.stream.listen((v) => print('second: $v'));
// throws: Stream has already been listened to.

The single-subscription controller is the right choice when the stream represents a sequence that must be consumed once. A file read, an HTTP response, a database query result, and a one-time event sequence are all single-subscription.

The onListen, onPause, onResume, and onCancel callbacks are available on the single-subscription controller. They fire for the single listener’s lifecycle:

final controller = StreamController<int>(
  onListen: () => print('listener attached'),
  onPause: () => print('paused'),
  onResume: () => print('resumed'),
  onCancel: () => print('canceled'),
);

The onListen callback fires when the listener attaches, which is the hook for starting the underlying resource. The onCancel callback fires when the listener cancels, which is the hook for cleaning up. The onPause and onResume callbacks fire when the subscription is paused and resumed.


b. The broadcast controller

The named constructor StreamController.broadcast() creates a broadcast stream. The stream property exposes the broadcast stream, and the add, addError, and close methods add events to it.

final controller = StreamController<int>.broadcast();

controller.stream.listen((v) => print('listener 1: $v'));
controller.stream.listen((v) => print('listener 2: $v'));

controller.add(1);
controller.add(2);
controller.close();

The output is:

listener 1: 1
listener 2: 1
listener 1: 2
listener 2: 2

Both listeners receive both events. The controller delivers each event to every current listener. A listener that attaches after an event is added does not receive that event, because the broadcast stream does not buffer.

The onListen callback fires when the first listener attaches, and the onCancel callback fires when the last listener cancels. The onPause callback fires when the last listener pauses, and the onResume callback fires when the first listener resumes. This is the lifecycle for the broadcast controller: the resource is started when the first listener attaches and stopped when the last listener cancels.

final controller = StreamController<int>.broadcast(
  onListen: () => print('first listener attached'),
  onCancel: () => print('last listener canceled'),
);

The broadcast controller is the right choice when the stream represents a source of events that multiple consumers observe. A user authentication state, a WebSocket connection, a sensor reading, and a global event bus are all broadcast streams.

The broadcast stream can be configured to buffer events for new listeners with the onListen callback, but the buffering is not automatic. The controller has no internal buffer. If the producer wants a new listener to receive the last event, it must store the event itself and deliver it in the onListen callback. This is the pattern for a “state stream” that always has a current value.


c. Choosing between the two and converting

The choice between a single-subscription and a broadcast stream depends on what the events represent and how many consumers need them.

Use a single-subscription stream when:

  • The stream represents a sequence that must be consumed in order from the beginning. A file read, an HTTP response body, a database query result, and a one-time event sequence are examples.
  • Only one consumer needs the events.
  • The events must not be lost. The buffering behavior of the single-subscription controller ensures that events added before the listener attaches are delivered.
  • The pause and resume behavior is needed, and the consumer wants to control the pace.

Use a broadcast stream when:

  • The stream represents a source of events that multiple consumers observe. A user authentication state, a WebSocket connection, a sensor reading, and a global event bus are examples.
  • Any number of consumers may attach and detach over time.
  • Events added before a listener attaches should be dropped. The stream represents the current state, not a replayable sequence.
  • The lifecycle is “start when the first listener attaches, stop when the last listener cancels.”

Converting a single-subscription stream to a broadcast stream. The asBroadcastStream() method converts a single-subscription stream to a broadcast stream. The original stream is listened to once, and the broadcast stream delivers the events to all its listeners.

final single = Stream.fromIterable([1, 2, 3]);
final broadcast = single.asBroadcastStream();

broadcast.listen((v) => print('first: $v'));
broadcast.listen((v) => print('second: $v'));

The conversion is one-way. A broadcast stream cannot be converted back to a single-subscription stream. The asBroadcastStream() method returns a broadcast stream that shares the original stream’s events. The original stream is paused when there are no listeners on the broadcast stream, and resumed when the first listener attaches.

The conversion has a caveat: the original stream is listened to once, and the broadcast stream distributes the events. If the original stream is a single-subscription stream that has already been listened to, the conversion fails. The conversion must happen before the original stream is listened to.

The state-stream pattern. A broadcast stream that always has a current value is called a state stream. The pattern is to store the last value in a field, and to deliver it in the onListen callback of the broadcast controller.

class StateStream<T> {
  T? _current;
  late final StreamController<T> _controller;

  StateStream() {
    _controller = StreamController<T>.broadcast(
      onListen: () {
        if (_current != null) {
          _controller.add(_current as T);
        }
      },
    );
  }

  Stream<T> get stream => _controller.stream;

  T? get value => _current;

  void add(T value) {
    _current = value;
    _controller.add(value);
  }

  void close() => _controller.close();
}

The StateStream class stores the current value and delivers it to each new listener. A listener that attaches late receives the current value immediately, and subsequent updates as they are added. This is the pattern for a stream that represents a state, such as the current user or the current theme.


Complete Example Session

// ============================================
// PART 1: A SINGLE-SUBSCRIPTION CONTROLLER
// ============================================

import 'dart:async';

void main() {
  final controller = StreamController<int>();

  controller.stream.listen(
    (value) => print('value: $value'),
    onError: (error) => print('error: $error'),
    onDone: () => print('done'),
  );

  controller.add(1);
  controller.add(2);
  controller.addError(Exception('boom'));
  controller.add(3);
  controller.close();
}

// Output:
// value: 1
// value: 2
// error: Exception: boom
// value: 3
// done


// ============================================
// PART 2: BUFFERING BEFORE THE LISTENER ATTACHES
// ============================================

void main() {
  final controller = StreamController<int>();

  controller.add(1);
  controller.add(2);
  controller.add(3);

  controller.stream.listen((v) => print('buffered: $v'));

  controller.close();
}

// Output:
// buffered: 1
// buffered: 2
// buffered: 3


// ============================================
// PART 3: THE SINGLE-LISTENER RESTRICTION
// ============================================

void main() {
  final controller = StreamController<int>();

  controller.stream.listen((v) => print('first: $v'));

  try {
    controller.stream.listen((v) => print('second: $v'));
  } catch (e) {
    print('error: $e');
  }
}

// Output:
// error: Bad state: Stream has already been listened to.


// ============================================
// PART 4: A BROADCAST CONTROLLER
// ============================================

void main() {
  final controller = StreamController<int>.broadcast();

  controller.stream.listen((v) => print('listener 1: $v'));
  controller.stream.listen((v) => print('listener 2: $v'));

  controller.add(1);
  controller.add(2);
  controller.close();
}

// Output:
// listener 1: 1
// listener 2: 1
// listener 1: 2
// listener 2: 2


// ============================================
// PART 5: NO BUFFERING IN A BROADCAST STREAM
// ============================================

void main() {
  final controller = StreamController<int>.broadcast();

  controller.add(1);
  controller.add(2);

  controller.stream.listen((v) => print('late listener: $v'));

  controller.add(3);
  controller.close();
}

// Output:
// late listener: 3


// ============================================
// PART 6: THE LIFECYCLE CALLBACKS
// ============================================

void main() {
  final controller = StreamController<int>(
    onListen: () => print('listener attached'),
    onPause: () => print('paused'),
    onResume: () => print('resumed'),
    onCancel: () => print('canceled'),
  );

  final sub = controller.stream.listen((v) => print(v));
  sub.pause();
  sub.resume();
  sub.cancel();
}

// Output:
// listener attached
// paused
// resumed
// canceled


// ============================================
// PART 7: THE BROADCAST LIFECYCLE
// ============================================

void main() {
  final controller = StreamController<int>.broadcast(
    onListen: () => print('first listener attached'),
    onCancel: () => print('last listener canceled'),
  );

  final sub1 = controller.stream.listen((v) => print('sub1: $v'));
  final sub2 = controller.stream.listen((v) => print('sub2: $v'));

  sub1.cancel();
  // onCancel does not fire; sub2 is still listening.

  sub2.cancel();
  // onCancel fires; the last listener canceled.
}

// Output:
// first listener attached
// last listener canceled


// ============================================
// PART 8: CONVERTING WITH asBroadcastStream()
// ============================================

void main() {
  final single = Stream.periodic(
    Duration(seconds: 1),
    (count) => count,
  ).take(3);

  final broadcast = single.asBroadcastStream();

  broadcast.listen((v) => print('first: $v'));
  broadcast.listen((v) => print('second: $v'));
}

// Both listeners receive the same events.
// The original stream is listened to once.


// ============================================
// PART 9: THE STATE-STREAM PATTERN
// ============================================

class StateStream<T> {
  T? _current;
  late final StreamController<T> _controller;

  StateStream() {
    _controller = StreamController<T>.broadcast(
      onListen: () {
        if (_current != null) {
          _controller.add(_current as T);
        }
      },
    );
  }

  Stream<T> get stream => _controller.stream;

  T? get value => _current;

  void add(T value) {
    _current = value;
    _controller.add(value);
  }

  void close() => _controller.close();
}

void main() {
  final state = StateStream<int>();

  state.add(1);
  state.add(2);

  state.stream.listen((v) => print('listener 1: $v'));
  // listener 1: 2 (the current value)

  state.add(3);
  // listener 1: 3

  state.stream.listen((v) => print('listener 2: $v'));
  // listener 2: 3 (the current value)
}


// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================

class EventBus {
  final _controller = StreamController<Object>.broadcast();

  Stream<Object> get stream => _controller.stream;

  void emit(Object event) {
    if (!_controller.isClosed) {
      _controller.add(event);
    }
  }

  void dispose() {
    _controller.close();
  }
}

void main() {
  final bus = EventBus();

  final sub1 = bus.stream.listen((e) => print('sub1: $e'));
  final sub2 = bus.stream.listen((e) => print('sub2: $e'));

  bus.emit('event 1');
  bus.emit('event 2');

  sub1.cancel();
  bus.emit('event 3');
  // Only sub2 receives event 3.

  bus.dispose();
}

// The EventBus is a broadcast stream that multiple
// subscribers observe. Each subscriber receives the
// events added after it attaches.

The ten parts show a single-subscription controller, buffering before the listener attaches, the single-listener restriction, a broadcast controller, no buffering in a broadcast stream, the lifecycle callbacks, the broadcast lifecycle, converting with asBroadcastStream(), the state-stream pattern, and the complete event-bus pattern.


Quick Reference

The Two Controllers

AspectStreamController()StreamController.broadcast()
ListenersOneMany
BufferingYesNo
Late listenersReceive buffered eventsMiss earlier events
LifecyclePer listenerFirst listen, last cancel
Use caseSequencesEvent streams

Controller Methods

MethodPurpose
add(value)Add a data event
addError(error)Add an error event
close()Close the stream
streamThe stream to listen to
isClosedWhether the controller is closed
hasListenerWhether a listener is attached

Lifecycle Callbacks

CallbackSingle-subscriptionBroadcast
onListenListener attachesFirst listener attaches
onPauseListener pausesLast listener pauses
onResumeListener resumesFirst listener resumes
onCancelListener cancelsLast listener cancels

Conversion

MethodDirection
asBroadcastStream()Single → broadcast
Cannot convert backBroadcast → single
Original streamListened to once
TimingBefore the original is listened to

Best Practices

✅ Do This:

// Use the default constructor for sequences
final controller = StreamController<int>();                               // ✅
// Use broadcast for multi-consumer events
final controller = StreamController<int>.broadcast();                     // ✅
// Close the controller when done
controller.close();                                                       // ✅
// Use onCancel to clean up resources
StreamController(onCancel: () => socket.close());                         // ✅
// Check isClosed before adding
if (!controller.isClosed) controller.add(value);                          // ✅
// Use a state stream for current-value semantics
class StateStream<T> { T? _current; ... }                                 // ✅

❌ Don’t Do This:

// Don't listen twice to a single-subscription stream
controller.stream.listen(...);
controller.stream.listen(...); // throws                                   // ❌
// Don't expect a broadcast stream to buffer
controller.add(1); // listener attaches later; event is lost               // ❌
// Don't forget to close the controller
controller.add(1); // never closed                                         // ❌
// Don't convert after the stream is listened to
stream.listen(...); stream.asBroadcastStream(); // too late                // ❌
// Don't use a broadcast stream for a one-shot sequence
Stream.fromIterable([1]).asBroadcastStream(); // unnecessary               // ❌
// Don't add to a closed controller
controller.close(); controller.add(1); // throws                           // ❌

Common Pitfalls

PitfallWhy It HappensFix
“Stream has already been listened to”Single-subscription, two listenersUse broadcast
Late listener misses eventsBroadcast does not bufferUse single-subscription or a state stream
Events lost before listener attachesBroadcast does not bufferBuffer manually or use single-subscription
onCancel does not fireBroadcast, other listeners remainExpected; fires when the last cancels
Controller not closedNo close() callClose when done
Add after closeController is closedCheck isClosed
Conversion failsStream already listened toConvert before listening

Real-World Examples

1. Single-Subscription Controller

final controller = StreamController<int>();
controller.stream.listen(print);
controller.add(1);
controller.close();

2. Broadcast Controller

final controller = StreamController<int>.broadcast();
controller.stream.listen(print);
controller.stream.listen(print);
controller.add(1);

3. Buffering

final controller = StreamController<int>();
controller.add(1);
controller.add(2);
controller.stream.listen(print); // receives 1, 2

4. No Buffering

final controller = StreamController<int>.broadcast();
controller.add(1);
controller.stream.listen(print); // does not receive 1

5. Lifecycle Callback

StreamController(onCancel: () => print('cleanup'));

6. Conversion

final broadcast = stream.asBroadcastStream();

7. State Stream

class StateStream<T> {
  T? _current;
  void add(T value) { _current = value; _controller.add(value); }
}

8. Event Bus

class EventBus {
  final _controller = StreamController<Object>.broadcast();
  void emit(Object event) => _controller.add(event);
}

9. Guard Against Closed

if (!controller.isClosed) controller.add(value);

10. Complete Pattern

final controller = StreamController<int>(
  onListen: () => start(),
  onCancel: () => stop(),
);

Visual

Single-Subscription vs Broadcast

┌──────────────────────────────────────────────────────────────┐
│  SINGLE-SUBSCRIPTION vs BROADCAST                            │
│                                                              │
│  SINGLE-SUBSCRIPTION:                                        │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  Controller ──► buffer ──► listener                  │    │
│  │                                                       │    │
│  │  One listener.                                       │    │
│  │  Events buffered until the listener attaches.        │    │
│  │  No events lost.                                     │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  BROADCAST:                                                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  Controller ──┬──► listener 1                        │    │
│  │               ├──► listener 2                        │    │
│  │               └──► listener 3                        │    │
│  │                                                       │    │
│  │  Many listeners.                                     │    │
│  │  No buffering.                                       │    │
│  │  Late listeners miss earlier events.                 │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Buffering Behavior

┌──────────────────────────────────────────────────────────────┐
│  THE BUFFERING BEHAVIOR                                      │
│                                                              │
│  SINGLE-SUBSCRIPTION:                                        │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  add(1) ──► buffer: [1]                              │    │
│  │  add(2) ──► buffer: [1, 2]                           │    │
│  │  add(3) ──► buffer: [1, 2, 3]                        │    │
│  │                                                       │    │
│  │  listen() ──► delivers 1, 2, 3                       │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  BROADCAST:                                                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  add(1) ──► no listeners ──► DROPPED                 │    │
│  │  add(2) ──► no listeners ──► DROPPED                 │    │
│  │                                                       │    │
│  │  listen() ──► attaches                                │    │
│  │  add(3) ──► delivers 3                                │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The Lifecycle Callbacks

┌──────────────────────────────────────────────────────────────┐
│  THE LIFECYCLE CALLBACKS                                     │
│                                                              │
│  SINGLE-SUBSCRIPTION:                                        │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  onListen  → the listener attaches                   │    │
│  │  onPause   → the listener pauses                     │    │
│  │  onResume  → the listener resumes                    │    │
│  │  onCancel  → the listener cancels                    │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  BROADCAST:                                                  │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  onListen  → the FIRST listener attaches             │    │
│  │  onPause   → the LAST listener pauses                │    │
│  │  onResume  → the FIRST listener resumes              │    │
│  │  onCancel  → the LAST listener cancels               │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  The broadcast lifecycle is "start on first, stop on last."  │
│                                                              │
└──────────────────────────────────────────────────────────────┘

The State-Stream Pattern

┌──────────────────────────────────────────────────────────────┐
│  THE STATE-STREAM PATTERN                                    │
│                                                              │
│  class StateStream<T> {                                      │
│    T? _current;                                              │
│    late final StreamController<T> _controller;               │
│                                                              │
│    StateStream() {                                           │
│      _controller = StreamController<T>.broadcast(            │
│        onListen: () {                                        │
│          if (_current != null) {                             │
│            _controller.add(_current as T);                   │
│          }                                                   │
│        },                                                    │
│      );                                                      │
│    }                                                         │
│                                                              │
│    void add(T value) {                                       │
│      _current = value;                                       │
│      _controller.add(value);                                 │
│    }                                                         │
│  }                                                           │
│                                                              │
│  ┌──────────────────────────────────────────────────────┐    │
│  │  state.add(1);                                       │    │
│  │  state.add(2);                                       │    │
│  │                                                       │    │
│  │  state.stream.listen(...)  → immediately receives 2  │    │
│  │                                                       │    │
│  │  state.add(3);             → receives 3              │    │
│  └──────────────────────────────────────────────────────┘    │
│                                                              │
│  A late listener receives the current value, not the         │
│  full history. This is the pattern for state streams.        │
│                                                              │
└──────────────────────────────────────────────────────────────┘

Summary

ItemValue
StreamController()Single-subscription stream
StreamController.broadcast()Broadcast stream
Single-subscription listenersOne
Broadcast listenersMany
Single-subscription bufferingYes
Broadcast bufferingNo
onListen (broadcast)First listener attaches
onCancel (broadcast)Last listener cancels
asBroadcastStream()Converts single to broadcast
State streamBroadcast with current-value delivery

Key takeaways:

  • StreamController() creates a single-subscription stream. Only one listener can attach. Events added before the listener attaches are buffered and delivered in order. The single listener can pause, resume, and cancel, and the lifecycle callbacks fire for that listener.
  • StreamController.broadcast() creates a broadcast stream. Any number of listeners can attach. Events added before a listener attaches are dropped. The lifecycle callbacks fire when the first listener attaches and the last listener cancels.
  • The buffering behavior is the key difference. A single-subscription stream buffers events until a listener attaches, so no events are lost. A broadcast stream does not buffer, so a late listener misses earlier events. This is the correct behavior for a state stream and the wrong behavior for a replayable sequence.
  • The lifecycle callbacks have different semantics. For a single-subscription controller, onListen fires when the listener attaches, and onCancel fires when the listener cancels. For a broadcast controller, onListen fires when the first listener attaches, and onCancel fires when the last listener cancels. The callbacks are the hook for starting and stopping the underlying resource.
  • asBroadcastStream() converts a single-subscription stream to a broadcast stream. The original stream is listened to once, and the broadcast stream distributes the events. The conversion is one-way and must happen before the original stream is listened to.
  • The state-stream pattern delivers the current value to late listeners. A broadcast controller with an onListen callback that adds the stored current value gives each new listener the current state immediately. This is the pattern for a stream that represents a state, such as the current user or theme.
  • Choose the variety based on what the events represent. A sequence to be consumed from the beginning is a single-subscription stream. A source of current events that multiple consumers observe is a broadcast stream.
  • Close the controller when done. The close() method closes the stream, and adding to a closed controller throws. The onCancel callback is the place to clean up the underlying resource.

Remember: A StreamController is the write end of a stream. The default constructor creates a single-subscription stream that buffers events and allows one listener. The broadcast constructor creates a broadcast stream that does not buffer and allows many listeners. The choice depends on whether the events are a sequence to be consumed from the beginning or a source of current events that multiple consumers observe. The asBroadcastStream() method converts a single-subscription stream to a broadcast stream, and the state-stream pattern delivers the current value to late listeners. The lifecycle callbacks are the hook for starting and stopping the underlying resource, with different semantics for the two varieties.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!