StreamControllers and Transformations
Prerequisites: Streams
async* functions are great when a stream's values come from a loop.
When values come from events — button taps, messages, updates from
elsewhere in your app — use a StreamController.
A broadcast stream of events
import 'dart:async';
class ChatRoom {
final _messages = StreamController<String>.broadcast();
Stream<String> get messages => _messages.stream;
void send(String text) => _messages.add(text);
Future<void> close() => _messages.close();
}
Future<void> main() async {
final room = ChatRoom();
room.messages.listen((m) => print('Ada sees: $m'));
room.messages.listen((m) => print('Linus sees: $m'));
room.send('hello');
room.send('bye');
await room.close();
}
Ada sees: hello
Linus sees: hello
Ada sees: bye
Linus sees: bye
- The class keeps the controller private and exposes only the
Stream— outside code can listen but can't add events. .broadcast()allows several listeners. Without it, the stream is single-subscription.- Close controllers when you're done with them.
Transforming streams
Future<void> main() async {
final searches = Stream.fromIterable(['d', 'da', 'da', 'dar', 'dart']);
final results = searches
.distinct()
.where((q) => q.length >= 2)
.asyncMap((q) async {
await Future.delayed(const Duration(milliseconds: 10));
return '${q.length} results for "$q"';
});
await for (final r in results) {
print(r);
}
}
2 results for "da"
3 results for "dar"
4 results for "dart"
distinct()drops consecutive duplicates.asyncMapruns an async function for each value, in order.
For more operators — debounce (wait for a pause in typing),
switchMap, combineLatest — the popular rxdart package extends
Dart streams.
Streams in Flutter
Flutter's StreamBuilder widget rebuilds part of the screen each time a
stream emits — for example, showing live chat messages or connection
status.
Common mistakes
- Adding to a closed controller:
Bad state: Cannot add event after closing. - Exposing the
StreamControlleritself instead of itsstream, letting any code inject events.