DMarlon Studio

Lesson 16 · 16 of 20

StreamController and events · Dart

Emit your own events in Dart with a StreamController and listen to them with listen, onError and onDone, using a shopping cart as the example.

Dart · StreamController and events in DMarlon Studio: the example's code and its output in the terminal

When events do not come from a loop but from one-off actions (a button tap, an incoming message), you use a `StreamController`. It has two sides: one to put events in (add, addError, close) and another, stream, to listen to them. Cart keeps the controller in a private field and only exposes events, so outside code can listen but not emit.

add updates the quantities and emits a message with the total number of items; remove emits a normal event if the item was there and, if not, an error through addError. A regular controller allows a single listener; created with `.broadcast()`, it allows any number, as here.

`listen` subscribes a function that receives each event and accepts two more: onError for errors and onDone for when the stream closes. The second listener, the cart badge, filters with where and only cares about additions. It still has an onError, even an empty one: on a broadcast stream, an error reaching a listener without onError counts as uncaught and stops the program.

Events are delivered asynchronously, but in the order they were emitted and to listeners in the order they subscribed, so the output is the same on every run. close() sends the done signal to everyone and returns a Future that main awaits, which is why cart closed comes last. This pattern, a class exposing a stream of changes, is the basis of architectures such as BLoC, widely used in Flutter.

Code · main.dart

import 'dart:async';class Cart {  final Map<String, int> _items = {};  // broadcast: any number of listeners can subscribe.  final _events = StreamController<String>.broadcast();  Stream<String> get events => _events.stream;  void add(String item) {    _items.update(item, (qty) => qty + 1, ifAbsent: () => 1);    _events.add('added $item, total ${_items.values.fold(0, (a, b) => a + b)}');  }  void remove(String item) => _items.remove(item) == null      ? _events.addError(StateError('$item is not in the cart'))      : _events.add('removed $item');  Future<void> close() => _events.close(); // sends "done" to every listener}Future<void> main() async {  final cart = Cart();  cart.events.listen((e) => print('log:   $e'),      onError: (Object e) => print('log:   ERROR ${(e as StateError).message}'),      onDone: () => print('log:   cart closed'));  cart.events.where((e) => e.startsWith('added')).listen((e) => print('badge: $e'),      onError: (_) {});  cart..add('Coffee beans')..add('Milk')..remove('Sugar')..remove('Milk')      ..add('Coffee beans');  await cart.close();}

Output · dart run main.dart

log:   added Coffee beans, total 1
badge: added Coffee beans, total 1
log:   added Milk, total 2
badge: added Milk, total 2
log:   ERROR Sugar is not in the cart
log:   removed Milk
log:   added Coffee beans, total 2
badge: added Coffee beans, total 2
log:   cart closed

Key points

  • A StreamController lets you emit events whenever you like.
  • add emits a value, addError an error and close closes the stream.
  • .broadcast() allows several listeners at once.
  • listen takes each event, plus onError and onDone.
  • Expose only the stream and keep the controller private.

Exercise

Add a clear() method that empties the cart and emits cleared, plus a third listener that counts the events it receives and prints the count in onDone.