Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.syntblaze.com/llms.txt

Use this file to discover all available pages before exploring further.

A Stream in Dart represents a sequence of asynchronous events. While an Iterable provides synchronous delivery of data, a Stream delivers data, errors, and a completion signal asynchronously over time. It is a core primitive of Dart’s asynchronous programming model, implementing the reactive programming pattern.

Event Types

A stream communicates with its subscribers by emitting three distinct types of events:
  1. Data Events: The actual payload of type T.
  2. Error Events: Exceptions or errors that occur during stream processing.
  3. Done Event: A terminal signal indicating no further data or error events will be emitted.

Stream Architectures

Dart categorizes streams into two architectural models based on subscription behavior:
  • Single-subscription Streams: The default stream type. It permits only one listener throughout its lifecycle. Attempting to listen a second time throws a StateError. The behavior of the stream before a subscription occurs depends on its instantiation: streams generated via async* are lazy and do not generate events until a listener attaches, whereas streams created manually via a StreamController will buffer emitted events internally to ensure no data is lost before the subscription occurs.
  • Broadcast Streams: Designed for multiple concurrent listeners. It does not buffer events; if an event is emitted when no listeners are attached, the event is permanently discarded. Listeners only receive events emitted after they subscribe.

Consuming Streams

Developers can consume streams using either asynchronous iteration or the subscription API. Asynchronous Iteration (await for) This syntax pauses execution of the surrounding async function until the stream emits a new event or completes.
Future<void> processStream(Stream<int> numberStream) async {
  await for (final int number in numberStream) {
    print(number); // Executes sequentially as data arrives
  }
  // Execution resumes here after the Done event is emitted
}
Subscription API (listen) The listen method returns a StreamSubscription<T>, providing granular control over the stream state (pause, resume, cancel) and explicit handlers for different event types.
import 'dart:async';

Future<void> manageSubscription() async {
  // Using a periodic stream to demonstrate asynchronous event emission over time
  final Stream<int> numberStream = Stream<int>.periodic(
    const Duration(milliseconds: 100),
    (int count) => count,
  );

  final StreamSubscription<int> subscription = numberStream.listen(
    (int number) => print('Data: $number'),
    onError: (Object error) => print('Error: $error'),
    onDone: () => print('Stream closed'),
    cancelOnError: false,
  );

  // Allow the stream to emit events before manipulating the subscription state
  await Future<void>.delayed(const Duration(milliseconds: 250));
  
  subscription.pause();
  await Future<void>.delayed(const Duration(milliseconds: 100));
  
  subscription.resume();
  await Future<void>.delayed(const Duration(milliseconds: 250));
  
  // Safely terminate the subscription before the stream naturally completes
  await subscription.cancel();
}

Creating Streams

Streams are typically instantiated via asynchronous generator functions or manual controllers. Asynchronous Generators (async*) The async* keyword defines a generator function that returns a Stream. The yield keyword emits individual data events, while yield* delegates to another stream.
Stream<int> generateNumbers(int max) async* {
  for (int i = 1; i <= max; i++) {
    yield i; // Emits a data event
  }
  // Function termination implicitly emits the Done event
}
StreamController A StreamController provides an imperative API to construct and manage a stream. It exposes a sink for event injection and a stream for subscription.
import 'dart:async';

void demonstrateController() {
  // Use StreamController<T>.broadcast() for a broadcast stream
  final StreamController<String> controller = StreamController<String>();

  // Exposing the stream
  final Stream<String> myStream = controller.stream;
  
  // Attaching a listener with an onError handler to catch asynchronous exceptions
  myStream.listen(
    (String event) => print('Received: $event'),
    onError: (Object error) => print('Caught error: $error'),
  );

  // Injecting events
  controller.sink.add('Event 1');        // Data event
  controller.sink.addError(Exception()); // Error event (caught by onError)
  controller.sink.close();               // Done event
}

Stream Transformations

Streams support functional composition. Methods like map, where, and expand return new streams that intercept and mutate events from the source stream before they reach the listener. For complex transformations, the transform method applies a StreamTransformer.
void transformStream() {
  final Stream<int> sourceStream = Stream<int>.fromIterable([1, 2, 3, 4, 5]);

  final Stream<int> transformedStream = sourceStream
      .where((int x) => x % 2 == 0) // Filters events
      .map((int x) => x * 10);      // Mutates events

  transformedStream.listen((int result) => print(result));
}
Master Dart with Deep Grasping Methodology!Learn More