Skip to content
AgileLogics
6 min read

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.
  • asyncMap runs 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 StreamController itself instead of its stream, letting any code inject events.