Streams
A Stream is a sequence of asynchronous values — events, ticks, location updates, websocket messages. Flutter wires them into the UI with StreamBuilder, and Dart provides rich operators (map/where/distinct/throttle) via the rxdart package. Streams are the right shape for anything that emits over time, where Future is single-value.
StreamBuilder, controllers, async generators, operators
EXAMPLE
import 'dart:async';
import 'package:flutter/material.dart';
// 1) Async generator — yields values over time, cleanest source for many streams
Stream<int> ticker(int max) async* {
for (var i = 1; i <= max; i++) {
await Future.delayed(const Duration(milliseconds: 300));
yield i;
}
}
// 2) StreamController — push values imperatively (e.g. from a websocket)
class CartCounter {
final _count = StreamController<int>.broadcast();
int _value = 0;
Stream<int> get stream => _count.stream;
void add(int delta) {
_value += delta;
_count.add(_value);
}
void dispose() => _count.close();
}
// 3) StreamBuilder — declarative UI bound to a Stream
class TickerPage extends StatelessWidget {
const TickerPage({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: ticker(10),
builder: (context, snap) {
if (snap.connectionState == ConnectionState.waiting) {
return const CircularProgressIndicator();
}
if (snap.hasError) return Text('Error: ${snap.error}');
return Text('Tick: ${snap.data}', style: const TextStyle(fontSize: 36));
},
);
}
}
// 4) Multiple subscribers — use a broadcast stream
final cart = CartCounter();
StreamBuilder<int>(
stream: cart.stream,
builder: (_, snap) => Badge.count(count: snap.data ?? 0),
);
// 5) Built-in operators — distinct, where, map, take, timeout
final positive = ticker(20)
.where((n) => n.isOdd)
.map((n) => 'odd-$n')
.take(5)
.timeout(const Duration(seconds: 3));
// 6) Hoist the stream into State; do not call ticker() inside build()
class CartBadge extends StatefulWidget {
const CartBadge({super.key, required this.counter});
final CartCounter counter;
@override
State<CartBadge> createState() => _CartBadgeState();
}
class _CartBadgeState extends State<CartBadge> {
late final Stream<int> _stream;
@override
void initState() { super.initState(); _stream = widget.counter.stream; }
@override
Widget build(BuildContext c) => StreamBuilder<int>(
stream: _stream,
builder: (_, snap) => Text('Cart: ${snap.data ?? 0}'),
);
}
// 7) Dispose controllers in StatefulWidget
class _SomeState extends State<StatefulWidget> {
final _ctl = StreamController<int>();
@override
void dispose() { _ctl.close(); super.dispose(); }
@override
Widget build(BuildContext c) => const SizedBox();
}
// 8) Async iteration — the cleanest consumer in non-UI code
Future<void> watchCart(CartCounter c) async {
await for (final v in c.stream) {
debugPrint('cart now $v');
if (v >= 10) break;
}
}
Why it matters
Always close StreamControllers in dispose() and prefer broadcast streams for state shared by multiple widgets. The cost of forgetting either: silent memory leaks, late callbacks setting state on disposed widgets, and the dreaded "Bad state: Stream has already been listened to" exception in production.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
Stream<int> ticks() async* {
int i = 0;
while (true) {
await Future.delayed(const Duration(seconds: 1));
yield ++i;
}
}
StreamBuilder<int>(
stream: ticks(),
builder: (_, s) => Text('${s.data ?? '-'}'),
)
Try it Yourself »
Discussion
Loading…