Skip to main content

Working with Streams and StreamBuilder

1. Understanding Stream Class

Property/Method Description Notes
Stream<T> Represents a sequence of asynchronous events Generic type parameter T defines the type of data emitted
StreamController<T> Manages a stream's lifecycle and allows manual emission of events Used to create custom streams or listen to existing ones
StreamSink<T> Interface for emitting events to a stream Part of StreamController, used to add data to the stream
StreamSubscription<T> Represents a subscription to a stream Allows listening and canceling the stream

Example: Creating a Stream

import 'dart:async';

void main() {
  // Create a stream controller
  StreamController<int> controller = StreamController<int>();

  // Add values to the stream
  controller.sink.add(1);
  controller.sink.add(2);
  controller.sink.add(3);

  // Close the stream
  controller.close();
}

Example: Listening to a Stream

import 'dart:async';

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

  // Listen to the stream
  controller.stream.listen((value) {
    print('Received: $value');
  });

  // Add values to the stream
  controller.sink.add(10);
  controller.sink.add(20);

  // Close the stream
  controller.close();
}
Note: Streams in Dart are lazy, meaning they only produce values when listened to.
Warning: Always close StreamController to prevent memory leaks.

2. Creating Stream Controllers

Property Description Values
StreamController<T> Creates a controller for managing streams StreamController<T>()
StreamController.broadcast Creates a broadcast stream controller StreamController.broadcast()
StreamController.fromFuture Creates a stream from a Future StreamController.fromFuture(future)
StreamController.fromIterable Creates a stream from an iterable StreamController.fromIterable(iterable)

Example: Creating a Stream Controller

import 'dart:async';

void main() {
  // Create a StreamController
  final controller = StreamController<String>();

  // Listen to the stream
  controller.stream.listen((data) {
    print('Received: $data');
  });

  // Add data to the stream
  controller.sink.add('Hello');
  controller.sink.add('World');

  // Close the stream
  controller.close();
}

Example: Broadcast Stream Controller

import 'dart:async';

void main() {
  // Create a broadcast stream controller
  final controller = StreamController<int>.broadcast();

  // Listen to the stream multiple times
  controller.stream.listen((data) {
    print('Listener 1: $data');
  });

  controller.stream.listen((data) {
    print('Listener 2: $data');
  });

  // Add data to the stream
  controller.sink.add(1);
  controller.sink.add(2);

  // Close the stream
  controller.close();
}

3. Adding Data to Streams

Method Description Usage
StreamController.add() Adds a single data item to the stream streamController.add("New Data");
StreamController.addError() Emits an error to the stream streamController.addError(Exception("Something went wrong"));
StreamController.close() Signals the end of data in the stream streamController.close();
Note: StreamController must be properly managed to avoid memory leaks. Always close controllers when done.

4. Listening to Streams

Method Description Usage
listen() Subscribes to a stream and handles events
stream.listen((data) {
  print(data);
});
forEach() Iterates over each event in the stream
stream.forEach((data) {
  print(data);
});
first Waits for and returns the first event
stream.first.then((data) {
  print(data);
});
firstWhere() Waits for and returns the first event matching a condition
stream.firstWhere((data) => data > 5).then((data) {
  print(data);
});
last Waits for and returns the last event
stream.last.then((data) {
  print(data);
});
lastWhere() Waits for and returns the last event matching a condition
stream.lastWhere((data) => data < 5).then((data) {
  print(data);
});

Example: Listening to a Stream

final stream = Stream<int>.periodic(Duration(seconds: 1), (i) => i + 1);
stream.listen((value) {
  print('Received: $value');
});
Note: Always manage stream subscriptions to prevent memory leaks.
Warning: Use StreamSubscription to control stream lifecycle.

5. Using StreamBuilder Widget

The StreamBuilder widget is a powerful tool in Flutter for building UIs that react to stream data. It listens to a stream and rebuilds its widget tree based on the latest data emitted by the stream.

Property Values Description
stream Stream<T> The stream to listen to. Required.
initialData T? Data to use before the stream emits any data.
builder Widget Function(BuildContext, AsyncSnapshot<T>) Function that builds the widget tree based on stream data.

Example: Basic StreamBuilder Usage

<div class="highlight">import 'package:flutter/material.dart';</div>

<div class="highlight">class StreamBuilderExample extends StatelessWidget {</div>
<div class="highlight">  @override</div>
<div class="highlight">  Widget build(BuildContext context) {</div>
<div class="highlight">    return StreamBuilder<int>(</div>
<div class="highlight">      stream: Stream.periodic(Duration(seconds: 1), (i) =&gt; i),</div>
<div class="highlight">      builder: (context, snapshot) {</div>
<div class="highlight">        if (snapshot.hasData) {</div>
<div class="highlight">          return Text('Value: ${snapshot.data}');</div>
<div class="highlight">        } else if (snapshot.hasError) {</div>
<div class="highlight">          return Text('Error: ${snapshot.error}');</div>
<div class="highlight">        } else {</div>
<div class="highlight">          return CircularProgressIndicator();</div>
<div class="highlight">        }</div>
<div class="highlight">      },</div>
<div class="highlight">    );</div>
<div class="highlight">  }</div>
<div class="highlight">}</div>
Note: The StreamBuilder widget is particularly useful for handling asynchronous data in Flutter, such as data from APIs or database queries.

Key Points

  • Reactive UI: Automatically rebuilds the widget tree when new data is available.
  • AsyncSnapshot: Provides access to the current state of the stream (loading, data, error).
  • Initial Data: Can provide initial data before the stream emits its first value.

6. Creating Broadcast Streams

Property Description Example
BroadcastStream Creates a stream that can be listened to by multiple listeners BroadcastStream<int> broadcastStream = Stream<int>.periodic(Duration(seconds: 1), (i) => i).asBroadcastStream();
asBroadcastStream() Converts a regular stream into a broadcast stream Stream<String> regularStream = Stream<String>.fromIterable(['a', 'b', 'c']); BroadcastStream<String> broadcastStream = regularStream.asBroadcastStream();

Example: Creating a Broadcast Stream

BroadcastStream<int> broadcastStream = Stream<int>.periodic(Duration(seconds: 1), (i) => i).asBroadcastStream();

void main() {
  broadcastStream.listen((data) {
    print('Received: $data');
  });
}

7. Transforming Streams

Method Description Example
map Transforms each element of the stream
stream
  .map((event) => event.toString().toUpperCase())
  .listen(print);
where Filters the stream to include only elements that satisfy a condition
stream
  .where((event) => event > 10)
  .listen(print);
take Take only the first n elements from the stream
stream
  .take(3)
  .listen(print);
skip Skip the first n elements from the stream
stream
  .skip(2)
  .listen(print);
distinct Removes duplicate elements from the stream
stream
  .distinct()
  .listen(print);
asyncMap Transforms each element asynchronously and flattens the result
stream
  .asyncMap((event) => Future.value(event * 2))
  .listen(print);
Note: Use asyncMap when you need to perform asynchronous operations on each element of the stream.

8. Merging and Combining Streams

Operation Description Code Example
Stream.merge Merges multiple streams into one, emitting events as they arrive from any of the input streams.
Stream<int> stream1 = Stream.fromIterable([1, 2]);
Stream<int> stream2 = Stream.fromIterable([3, 4]);
Stream<int> merged = Stream.merge([stream1, stream2]);
// Emits: 1, 2, 3, 4
Stream.combine Combines multiple streams using a combinator function to produce a single stream of results.
Stream<int> stream1 = Stream.fromIterable([1, 2]);
Stream<int> stream2 = Stream.fromIterable([3, 4]);
Stream<String> combined = Stream.combine(
  [stream1, stream2],
  (List<Object?> values) => '${values[0]} and ${values[1]}',
);
// Emits: "1 and 3", "2 and 4"
Stream.zip Zips multiple streams together, emitting a list of values when all streams have emitted one value.
Stream<int> stream1 = Stream.fromIterable([1, 2]);
Stream<int> stream2 = Stream.fromIterable([3, 4]);
Stream<List<int>> zipped = Stream.zip([stream1, stream2], (list) => list);
// Emits: [1, 3], [2, 4]
Stream.switchMap Transforms each event from a stream into another stream and switches to the latest one.
Stream<int> stream1 = Stream.fromIterable([1, 2]);
Stream<Stream<int>> streamOfStreams = Stream.fromIterable([
  Stream.fromIterable([10, 20]),
  Stream.fromIterable([30, 40])
]);
Stream<int> switched = streamOfStreams.asyncExpand((stream) => stream);
// Emits: 10, 20, 30, 40
Note: Stream.combine and Stream.zip require all input streams to emit values in sync or they may not emit until all have completed.

9. Canceling Stream Subscriptions

When working with streams in Flutter, it's important to properly cancel subscriptions to prevent memory leaks and unnecessary resource consumption.

Method Description Usage
StreamSubscription.cancel() Stops the stream subscription and releases associated resources. subscription.cancel();

Example: Canceling a Stream Subscription

final subscription = stream.listen((data) {
  print(data);
});

// Cancel the subscription when no longer needed
subscription.cancel();
Note: Canceling a stream subscription is especially important in widgets that are disposed of, such as in StatefulWidget when the widget is removed from the tree.

Best Practices for Stream Cancellation

  • Always cancel stream subscriptions in dispose() methods of widgets.
  • Use StreamSubscription objects to manage and cancel subscriptions.
  • Consider using StreamBuilder or FutureBuilder for automatic cleanup in some cases.

10. Handling Stream Errors

Error Type Description Handling Strategy
StreamError Errors thrown by a stream during data emission Use StreamBuilder's error parameter to display or log errors
asyncError Errors in asynchronous operations within a stream Wrap async logic in try-catch and propagate errors via addError

Example: Error Handling in StreamBuilder

StreamBuilder<String>(
  stream: myStream,
  builder: (context, snapshot) {
    if (snapshot.hasError) {
      return Text('Error: ${snapshot.error}');
    }
    // Handle other states...
  },
)
Note: Always handle errors in streams to prevent uncaught exceptions from crashing the app.

11. Using async* for Stream Generation

The async* keyword in Dart allows you to define a generator function that returns a Stream. This is particularly useful for generating streams of data over time, such as from a database query or an API call.

Feature Description Example
async* Defines a generator function that returns a Stream
Stream<int> countUp(int to) async* {
  for (int i = 0; i < to; i++) {
    yield i;
  }
}
yield Emits a value into the stream
Stream<String> getNames() async* {
  yield 'Alice';
  yield 'Bob';
}
yield* Yields all values from another iterable or stream
Stream<int> mergeStreams(Stream<int> stream1, Stream<int> stream2) async* {
  yield* stream1;
  yield* stream2;
}

Example: Generating a Stream of Numbers

Stream<int> countUp(int to) async* {
  for (int i = 0; i < to; i++) {
    yield i;
  }
}

// Usage
void main() {
  countUp(5).listen((value) {
    print(value);
  });
}

Example: Combining Streams

Stream<int> mergeStreams(Stream<int> stream1, Stream<int> stream2) async* {
  yield* stream1;
  yield* stream2;
}

// Usage
void main() {
  final stream1 = Stream.fromIterable([1, 2, 3]);
  final stream2 = Stream.fromIterable([4, 5, 6]);
  
  mergeStreams(stream1, stream2).listen((value) {
    print(value);
  });
}

Was this article helpful?