| | |

Dart 40 ๐ŸŽฏ Asynchronous Data Streams: Stream Fundamentals and Creation

A Future delivers exactly one value โ€” or one error โ€” at one point in time, and then it is done. A Stream delivers a sequence of values over time. The difference is not just quantity. A future is a placeholder for a single asynchronous result; a stream is a channel for asynchronous events. A user typing into a field produces a stream of keystrokes. A file being read produces a stream of byte chunks. A WebSocket connection produces a stream of messages. Any operation that emits multiple values over time is a stream, and Dart’s Stream class is the abstraction that represents it.

The mental model is closest to a conveyor belt: values are placed on the belt one at a time, and they arrive at the consumer in order. A stream can emit any number of values, including zero. It can emit an error at any point, and after an error it can continue emitting values or close. When the stream is done, it emits a “done” event, and no further events follow. The consumer can listen to the stream, pause the subscription, resume it, or cancel it. The stream itself does not run until someone listens to it โ€” this is a critical property, and it is the difference between a stream that is a passive description of a sequence and a stream that is an active source of events.

Streams come in two fundamental varieties: single-subscription streams and broadcast streams. A single-subscription stream allows only one listener for its entire lifetime. The events are delivered in order to that one listener, and once the listener cancels, the stream is done. A broadcast stream allows any number of listeners, and events are delivered to all of them. The trade-off is buffering: a single-subscription stream buffers events until a listener attaches, so no data is lost; a broadcast stream does not buffer, so a listener that attaches late misses earlier events. The choice between them depends on whether the event source is inherently single-consumer or multi-consumer.

This chapter covers three areas. First, why streams exist โ€” the problem of representing sequences of asynchronous events, and how streams differ from futures. Second, how streams work โ€” the anatomy of a stream, the listen method, the subscription object, and the lifecycle of events. Third, how to create streams โ€” the Stream constructors, StreamController, async generators, and the transformation of existing sources into streams. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the stream lifecycle and the two subscription models.

Key point: A Stream is a sequence of asynchronous events. It can emit any number of values, zero or more errors, and a done event. A single-subscription stream allows one listener and buffers events until that listener attaches. A broadcast stream allows multiple listeners and does not buffer. A stream does nothing until it is listened to.


Why streams exist

The repeated-value problem. A Future represents one value. When an operation produces multiple values over time โ€” keystrokes, sensor readings, server messages, file chunks โ€” a future cannot represent it. A Future<List<int>> could represent a batch of values collected at one point, but it cannot represent values arriving incrementally. The consumer would have to wait for the entire batch before processing any of it, and there would be no way to react to the first value before the last one arrives. A Stream represents the sequence itself. Each value is delivered as it becomes available, and the consumer can process each one immediately .

The push-vs-pull problem. A future is pull-based in a specific sense: the consumer attaches a callback, and the value is pushed when ready. But the future itself represents a single completion. A stream is explicitly push-based: the source pushes events to the consumer as they occur, and the consumer reacts to each one. The consumer does not ask for the next value; the source decides when to emit. This matches the nature of event sources: a user presses a key when they press it, a server sends a message when it sends it, a sensor reads a value when it is read. The stream is the interface between a push-based source and a consumer that processes events as they arrive .

The backpressure problem. A push-based source can produce events faster than the consumer can process them. A stream addresses this with pause and resume. The subscription returned by listen has pause() and resume() methods. When the subscription is paused, the source buffers events (for a single-subscription stream) or drops them (for a broadcast stream). The consumer can pause when it is not ready to process and resume when it is. This is cooperative flow control: the source does not overwhelm the consumer, and the consumer controls the pace. Without this mechanism, a fast producer would flood a slow consumer, and events would be lost or memory would grow unbounded .

The single-vs-broadcast problem. Some event sources have one logical consumer. A file read produces bytes for whoever is reading the file. A single-subscription stream models this: one listener, events delivered in order, buffering until the listener attaches. Other sources have multiple logical consumers. A stream of user authentication changes might be needed by the navigation bar, the profile page, and the settings screen all at once. A broadcast stream models this: any number of listeners, events delivered to all, no buffering. The two models reflect two different realities, and the choice between them is a statement about how many consumers the source supports .

The lifecycle problem. A stream has a lifecycle: it can be listened to, paused, resumed, and canceled. It can emit values, errors, and a done event. The done event marks the end of the sequence, and after it, no further events are delivered. This lifecycle is what makes streams composable: they can be transformed with map, where, expand, and other operators, and the transformations preserve the lifecycle. A stream can be listened to once and transformed many times, and the resulting stream is itself a stream with the same lifecycle rules. This composability is what allows a stream-based architecture to describe complex event processing as a pipeline of simple transformations .

The trade-off. Streams introduce complexity that futures do not have. A future has one completion; a stream has many events, an error channel, and a done event. A single-subscription stream can only be listened to once, which is a common source of bugs when a widget rebuilds and tries to listen again. A broadcast stream drops events for listeners that attach late, which is a different kind of bug. The stream lifecycle requires careful management: subscriptions must be canceled to avoid leaks, and errors must be handled or they propagate to the zone error handler. The trade-off is between the power of representing sequences of asynchronous events and the discipline required to manage subscriptions, cancellation, and the single-vs-broadcast distinction.


a. The anatomy of a stream

A Stream<T> is a sequence of events of type T. Each event is either a data event (a value of type T), an error event, or a done event. The stream can emit any number of data events, zero or more error events, and exactly one done event. After the done event, no further events are emitted.

A stream is consumed with listen:

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

The listen method returns a StreamSubscription. The subscription represents the active connection between the stream and the consumer. It has three lifecycle methods:

  • pause() โ€” stop delivering events. For a single-subscription stream, events are buffered. For a broadcast stream, events are dropped.
  • resume() โ€” resume delivering events.
  • cancel() โ€” stop listening. The subscription is closed, and no further events are delivered. cancel() returns a future that completes when the cancellation is processed.

The onError callback can take one argument (the error) or two (the error and the stack trace). If onError is omitted, errors are forwarded to the zone’s error handler, which in a Flutter app produces a red error screen in debug mode .

A stream does not run until it is listened to. A single-subscription stream is a description of a sequence; the sequence begins when the first listener attaches. This is why a stream can be created and stored without any side effects, and why the act of listening is what starts the work.

b. Single-subscription vs broadcast streams

A single-subscription stream allows exactly one listener for its entire lifetime. The first call to listen attaches the listener. A second call to listen throws a StateError. The stream buffers events until the listener attaches, so no data is lost if the listener is not yet ready. The events are delivered in order to that one listener .

final stream = Stream.fromIterable([1, 2, 3]);

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

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

The buffering behavior means that a single-subscription stream can be created, and its events can be produced, before any listener attaches. The events are queued and delivered when listen is called. This is useful when the stream represents a resource that should be read from the beginning, such as a file or an HTTP response body.

A broadcast stream allows any number of listeners. Each listener receives events from the point at which it attaches. Events emitted before a listener attaches are not delivered to that listener โ€” there is no buffering. This is the correct behavior for an event source that represents “what is happening now,” such as a user input stream or a WebSocket message stream .

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

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

controller.add(1);
// listener 1: 1
// listener 2: 1

A single-subscription stream can be converted to a broadcast stream with asBroadcastStream(). This returns a broadcast stream that shares the original stream’s events. The original stream is listened to once, and the broadcast stream delivers the events to all its listeners. The conversion is one-way: a broadcast stream cannot be converted back to a single-subscription stream.

c. Creating streams

Streams can be created in several ways, each suited to a different source.

From a collection. Stream.fromIterable converts an iterable into a stream. The values are emitted one at a time, asynchronously:

final stream = Stream.fromIterable([1, 2, 3]);

From a future. Stream.fromFuture creates a stream that emits one value when the future completes, then closes:

final stream = Stream.fromFuture(fetchData());

From a periodic timer. Stream.periodic creates a stream that emits values at a fixed interval. The values are computed by a callback that receives the event index:

final stream = Stream.periodic(
  Duration(seconds: 1),
  (count) => count,
);

From a value. Stream.value creates a stream that emits one value and closes:

final stream = Stream.value(42);

From an empty stream. Stream.empty creates a stream that emits no values and closes immediately:

final stream = Stream.empty();

From an error. Stream.error creates a stream that emits one error and closes:

final stream = Stream.error(Exception('boom'));

From a controller. StreamController is the most flexible way to create a stream. It exposes a stream property and an add method. Events added to the controller are delivered to the stream’s listeners:

final controller = StreamController<int>();

controller.stream.listen((v) => print(v));

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

The controller has a stream property that is the single-subscription stream. A broadcast controller is created with StreamController.broadcast() and its stream is a broadcast stream. The controller must be closed with close() when no more events will be added. The onListen, onPause, onResume, and onCancel callbacks can be provided to react to the subscription lifecycle.

From an async generator. An async* function returns a stream. The yield keyword emits a value, and yield* delegates to another stream:

Stream<int> countTo(int n) async* {
  for (var i = 1; i <= n; i++) {
    yield i;
  }
}

The function body runs when the stream is listened to. Each yield emits a value. The function can await between yields, and the stream will pause until the await completes. The yield* form delegates to another stream, emitting all of its events .


Complete Example Session

// ============================================
// PART 1: A BASIC STREAM
// ============================================

// Stream.fromIterable creates a stream that
// emits each value in the iterable.

void main() {
  final stream = Stream.fromIterable([1, 2, 3]);

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

// Output:
// value: 1
// value: 2
// value: 3
// done


// ============================================
// PART 2: THE SUBSCRIPTION LIFECYCLE
// ============================================

// listen returns a subscription that can be
// paused, resumed, and canceled.

final subscription = Stream.periodic(
  Duration(seconds: 1),
  (count) => count,
).listen((value) => print('tick: $value'));

// Later:
subscription.pause();  // stop delivering events
subscription.resume(); // resume delivery
subscription.cancel(); // stop listening permanently


// ============================================
// PART 3: SINGLE-SUBSCRIPTION RESTRICTION
// ============================================

// A single-subscription stream can only be
// listened to once.

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

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

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


// ============================================
// PART 4: BROADCAST STREAM
// ============================================

// A broadcast stream allows multiple listeners.
// Events are delivered to all of them.

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: SINGLE-SUBSCRIPTION STREAM CONTROLLER
// ============================================

// A single-subscription controller buffers
// events until a listener attaches.

final singleController = StreamController<int>();

singleController.add(1); // buffered
singleController.add(2); // buffered

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

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


// ============================================
// PART 6: ASYNC GENERATOR
// ============================================

// An async* function returns a stream. Each
// yield emits a value.

Stream<int> countTo(int n) async* {
  for (var i = 1; i <= n; i++) {
    await Future.delayed(Duration(milliseconds: 100));
    yield i;
  }
}

void main() async {
  await for (final value in countTo(3)) {
    print('count: $value');
  }
}

// Output (100ms apart):
// count: 1
// count: 2
// count: 3


// ============================================
// PART 7: yield* โ€” DELEGATING TO ANOTHER STREAM
// ============================================

Stream<int> inner() async* {
  yield 1;
  yield 2;
}

Stream<int> outer() async* {
  yield 0;
  yield* inner(); // emits 1, 2
  yield 3;
}

// Output: 0, 1, 2, 3


// ============================================
// PART 8: ERROR EVENTS
// ============================================

// A stream can emit errors. The onError callback
// receives them.

final errorController = StreamController<int>();

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

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

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


// ============================================
// PART 9: STREAM TRANSFORMATIONS
// ============================================

// Streams can be transformed with map, where,
// and other operators.

Stream.fromIterable([1, 2, 3, 4, 5])
    .where((n) => n.isEven)
    .map((n) => n * 10)
    .listen((v) => print(v));

// Output:
// 20
// 40


// ============================================
// PART 10: CANCELING A SUBSCRIPTION
// ============================================

// cancel() stops the subscription. For a
// controller-backed stream, it triggers onCancel.

final cancelController = StreamController<int>(
  onCancel: () => print('subscription canceled'),
);

final sub = cancelController.stream.listen((v) => print(v));
cancelController.add(1);

// Later:
sub.cancel(); // prints 'subscription canceled'

The ten parts show basic stream listening, the subscription lifecycle, the single-subscription restriction, broadcast streams, buffering, async generators, yield*, error events, transformations, and cancellation.


Quick Reference

Stream Event Types

EventMeaning
DataA value of type T
ErrorAn error and optional stack trace
DoneThe stream has closed; no further events

listen Parameters

ParameterPurpose
onDataCalled for each data event
onErrorCalled for each error event
onDoneCalled when the stream closes
cancelOnErrorIf true, cancels the subscription on the first error

Subscription Methods

MethodEffect
pause()Stop delivering events (buffered or dropped)
resume()Resume delivering events
cancel()Stop listening permanently

Single-Subscription vs Broadcast

AspectSingle-subscriptionBroadcast
ListenersOneMany
BufferingYes, until listener attachesNo
Late listenersReceive all buffered eventsMiss earlier events
ConversionasBroadcastStream()Cannot convert back

Stream Constructors

ConstructorPurpose
Stream.fromIterableEmit each item in an iterable
Stream.fromFutureEmit one value from a future
Stream.periodicEmit values at a fixed interval
Stream.valueEmit one value and close
Stream.emptyEmit nothing and close
Stream.errorEmit one error and close
StreamControllerFull control over event emission
async* functionEmit values with yield

Best Practices

โœ… Do This:

// Cancel subscriptions when done
final sub = stream.listen((v) => print(v));
sub.cancel();                                                          // โœ…
// Use broadcast for multi-consumer streams
final controller = StreamController<int>.broadcast();                  // โœ…
// Handle errors
stream.listen((v) => print(v), onError: (e) => print(e));              // โœ…
// Close controllers
controller.close();                                                    // โœ…
// Use async* for generated sequences
Stream<int> count() async* { yield 1; yield 2; }                       // โœ…
// Use await for to consume streams
await for (final v in stream) { print(v); }                            // โœ…

โŒ Don’t Do This:

// Don't listen to a single-subscription stream twice
stream.listen((v) => print(v));
stream.listen((v) => print(v)); // throws                                // โŒ
// Don't forget to close a controller
controller.add(1); // controller never closed                           // โŒ
// Don't ignore errors
stream.listen((v) => print(v)); // no onError                           // โŒ
// Don't use a broadcast stream for one-shot data
final broadcast = Stream.fromIterable([1]).asBroadcastStream();         // โŒ
// Don't leak subscriptions
stream.listen((v) => print(v)); // no cancel                            // โŒ

Common Pitfalls

PitfallWhy It HappensFix
Stream already listened toSingle-subscription streamUse broadcast or asBroadcastStream
Late listener misses eventsBroadcast stream does not bufferUse single-subscription or attach early
Subscription leakcancel() not calledCancel in dispose or when done
Controller not closedclose() not calledClose when no more events
Error unhandledNo onErrorAdd onError or catchError
Async generator not runningNot listened toStreams are lazy; listen to start
Events out of orderMultiple async sourcesSingle stream preserves order

Real-World Examples

1. From Iterable

Stream.fromIterable([1, 2, 3]).listen(print);

2. From Future

Stream.fromFuture(fetchData()).listen(print);

3. Periodic Timer

Stream.periodic(Duration(seconds: 1), (i) => i).listen(print);

4. Single Value

Stream.value(42).listen(print);

5. Error Stream

Stream.error(Exception('boom')).listen(print, onError: print);

6. StreamController

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

7. Broadcast Controller

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

8. Async Generator

Stream<int> gen() async* { yield 1; yield 2; }

9. yield*

Stream<int> outer() async* { yield 0; yield* inner(); }

10. Transform

stream.where((n) => n > 0).map((n) => n * 2).listen(print);

Visual

Stream Lifecycle

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  STREAM LIFECYCLE                                            โ”‚
โ”‚                                                              โ”‚
โ”‚  Stream created                                              โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ”‚  (no events until listen)                               โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  listen() called                                             โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ”œโ”€โ”€โ–บ data event โ”€โ”€โ–บ onData                                โ”‚
โ”‚    โ”œโ”€โ”€โ–บ data event โ”€โ”€โ–บ onData                                โ”‚
โ”‚    โ”œโ”€โ”€โ–บ error event โ”€โ–บ onError                               โ”‚
โ”‚    โ”œโ”€โ”€โ–บ data event โ”€โ”€โ–บ onData                                โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  close() called                                              โ”‚
โ”‚    โ”‚                                                         โ”‚
โ”‚    โ–ผ                                                         โ”‚
โ”‚  done event โ”€โ”€โ–บ onDone                                       โ”‚
โ”‚                                                              โ”‚
โ”‚  Subscription methods:                                       โ”‚
โ”‚    pause()  โ”€โ”€โ–บ events buffered (single) or dropped (broadcast)
โ”‚    resume() โ”€โ”€โ–บ events delivered again                       โ”‚
โ”‚    cancel() โ”€โ”€โ–บ subscription closed, no more events          โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Single-Subscription vs Broadcast

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  SINGLE-SUBSCRIPTION                                         โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Stream โ”€โ”€โ–บ buffers โ”€โ”€โ–บ listener                     โ”‚    โ”‚
โ”‚  โ”‚                                                       โ”‚    โ”‚
โ”‚  โ”‚  Only one listener. Events buffered until it          โ”‚    โ”‚
โ”‚  โ”‚  attaches. No events lost.                           โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค
โ”‚  BROADCAST                                                   โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  Stream โ”€โ”€โ”ฌโ”€โ”€โ–บ listener 1                            โ”‚    โ”‚
โ”‚  โ”‚           โ”œโ”€โ”€โ–บ listener 2                            โ”‚    โ”‚
โ”‚  โ”‚           โ””โ”€โ”€โ–บ listener 3                            โ”‚    โ”‚
โ”‚  โ”‚                                                       โ”‚    โ”‚
โ”‚  โ”‚  Many listeners. No buffering. Late listeners         โ”‚    โ”‚
โ”‚  โ”‚  miss earlier events.                                โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Use single-subscription for one-shot data (file, HTTP).     โ”‚
โ”‚  Use broadcast for ongoing events (input, WebSocket).        โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Event Ordering

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  EVENT ORDERING                                              โ”‚
โ”‚                                                              โ”‚
โ”‚  controller.add(1);                                          โ”‚
โ”‚  controller.addError(Exception('boom'));                     โ”‚
โ”‚  controller.add(2);                                          โ”‚
โ”‚  controller.close();                                         โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  onData:  1                                          โ”‚    โ”‚
โ”‚  โ”‚  onError: Exception: boom                            โ”‚    โ”‚
โ”‚  โ”‚  onData:  2                                          โ”‚    โ”‚
โ”‚  โ”‚  onDone:  (stream closed)                            โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  Errors do not close the stream. The stream continues        โ”‚
โ”‚  emitting events after an error until close() is called.     โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Async Generator

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  ASYNC GENERATOR                                             โ”‚
โ”‚                                                              โ”‚
โ”‚  Stream<int> countTo(int n) async* {                         โ”‚
โ”‚    for (var i = 1; i <= n; i++) {                            โ”‚
โ”‚      await Future.delayed(Duration(seconds: 1));             โ”‚
โ”‚      yield i;                                                โ”‚
โ”‚    }                                                         โ”‚
โ”‚  }                                                           โ”‚
โ”‚                                                              โ”‚
โ”‚  โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”‚
โ”‚  โ”‚  listen() called                                     โ”‚    โ”‚
โ”‚  โ”‚    โ”‚                                                 โ”‚    โ”‚
โ”‚  โ”‚    โ–ผ                                                 โ”‚    โ”‚
โ”‚  โ”‚  loop starts                                         โ”‚    โ”‚
โ”‚  โ”‚    โ”‚                                                 โ”‚    โ”‚
โ”‚  โ”‚    โ”œโ”€โ”€ i=1: await, yield 1                           โ”‚    โ”‚
โ”‚  โ”‚    โ”œโ”€โ”€ i=2: await, yield 2                           โ”‚    โ”‚
โ”‚  โ”‚    โ”œโ”€โ”€ i=3: await, yield 3                           โ”‚    โ”‚
โ”‚  โ”‚    โ”‚                                                 โ”‚    โ”‚
โ”‚  โ”‚    โ–ผ                                                 โ”‚    โ”‚
โ”‚  โ”‚  function ends, stream closes                        โ”‚    โ”‚
โ”‚  โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ”‚
โ”‚                                                              โ”‚
โ”‚  The function body runs when listened to. Each yield         โ”‚
โ”‚  emits a value. The stream pauses at each await.             โ”‚
โ”‚                                                              โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Summary

ItemValue
Stream<T>Sequence of asynchronous events of type T
Event typesData, error, done
listenAttaches a listener; returns a StreamSubscription
pause/resume/cancelSubscription lifecycle methods
Single-subscriptionOne listener; buffers events
BroadcastMany listeners; no buffering
asBroadcastStreamConverts single-subscription to broadcast
StreamControllerFull control over event emission
async* / yieldGenerator that returns a stream
LazyStreams do nothing until listened to

Key takeaways:

  • A stream is a sequence of asynchronous events. Unlike a future, which delivers one value, a stream can deliver any number of values, zero or more errors, and a done event .
  • A stream does nothing until it is listened to. The act of calling listen starts the sequence. This is why streams can be created and stored without side effects.
  • Single-subscription streams allow one listener and buffer events. The first listen attaches the listener. A second listen throws. Events emitted before the listener attaches are buffered and delivered in order .
  • Broadcast streams allow many listeners and do not buffer. Each listener receives events from the point it attaches. Events emitted before a listener attaches are missed. Use broadcast for ongoing event sources with multiple consumers .
  • StreamController is the most flexible way to create a stream. It exposes a stream property and add, addError, and close methods. The onListen, onPause, onResume, and onCancel callbacks react to the subscription lifecycle.
  • async* functions return streams. The yield keyword emits a value, and the function body runs when the stream is listened to. The yield* form delegates to another stream, emitting all of its events .
  • Errors do not close a stream. An error event is delivered to the onError callback, and the stream continues emitting data events until close() is called. This allows a stream to survive errors and continue .
  • Cancel subscriptions to avoid leaks. A subscription that is not canceled keeps the stream and its resources alive. In a widget-based framework, subscriptions should be canceled when the widget is disposed.

Remember: A Stream is the abstraction for a sequence of asynchronous events. It can emit any number of values, zero or more errors, and a done event. The two varieties โ€” single-subscription and broadcast โ€” model two different realities: one consumer with buffering, or many consumers without. The StreamController and async* generators are the primary ways to create streams, and Stream.fromIterable, Stream.periodic, and the other constructors cover the common cases. A stream is lazy: it does nothing until listened to, and the subscription controls the flow with pause, resume, and cancel. Understanding these fundamentals is the prerequisite for transforming streams, combining them, and using them in a reactive architecture.



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!