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 | |
| forEach() | Iterates over each event in the stream | |
| first | Waits for and returns the first event | |
| firstWhere() | Waits for and returns the first event matching a condition | |
| last | Waits for and returns the last event | |
| lastWhere() | Waits for and returns the last event matching a condition | |
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) => 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 | |
| where | Filters the stream to include only elements that satisfy a condition | |
| take | Take only the first n elements from the stream | |
| skip | Skip the first n elements from the stream | |
| distinct | Removes duplicate elements from the stream | |
| asyncMap | Transforms each element asynchronously and flattens the result | |
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.combine | Combines multiple streams using a combinator function to produce a single stream of results. | |
| Stream.zip | Zips multiple streams together, emitting a list of values when all streams have emitted one value. | |
| Stream.switchMap | Transforms each event from a stream into another stream and switches to the latest one. | |
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
StreamSubscriptionobjects to manage and cancel subscriptions. - Consider using
StreamBuilderorFutureBuilderfor 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 | |
| yield | Emits a value into the stream | |
| yield* | Yields all values from another iterable or stream | |
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);
});
}