iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

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…