Explain Dart's Stream and when to use it

High PriorityAsked in ~70% of Flutter interviews

4 min read

Dart Fundamentals

A Future is one async value; a Stream is many async values over time — think async iterator. Use streams whenever data trickles in: WebSocket messages, sensor readings, Firestore updates, search input, BLoC state.

Future<T>Stream<T>
Values emittedExactly oneZero, one, or many
CompletesOnceWhen close() is called (or never)
Consume withawaitawait for / .listen()
Typical useHTTP call, DB queryWebSocket, taps, real-time updates

A stream has two ends: a sink (where you push values in) and the stream (where consumers listen). It can finish with onDone or blow up with onError.


Code in action

// Creating a stream — 3 common ways

// 1. async* generator (most ergonomic)
Stream<int> countDown(int from) async* {
  for (var i = from; i >= 0; i--) {
    yield i;
    await Future.delayed(const Duration(seconds: 1));
  }
}

// 2. Stream.periodic — emit on an interval
final ticker = Stream.periodic(const Duration(seconds: 1), (i) => i).take(5);

// 3. StreamController — when you need manual push
final ctrl = StreamController<String>();
ctrl.sink.add('Hello');
ctrl.sink.add('World');
await ctrl.close();
// Consuming a stream

// listen() — most flexible
final sub = stream.listen(
  (data)  => print('got $data'),
  onError: (e) => print('error: $e'),
  onDone:  () => print('stream closed'),
);
await sub.cancel();  // remember to cancel!

// await for — reads top-to-bottom
await for (final value in stream) {
  print(value);
}

Single-subscription vs Broadcast

// Default: single-subscription — only ONE listener ever
final s = StreamController<int>();
s.stream.listen(print);
s.stream.listen(print);     // ❌ throws — already listened

// Broadcast — multiple listeners, no replay of past events
final b = StreamController<int>.broadcast();
b.stream.listen((v) => print('A: $v'));
b.stream.listen((v) => print('B: $v'));   // ✅ both receive

Single = one consumer, buffers events. Broadcast = many consumers, fire-and-forget.


Real-world: search with debounce

Stream<List<Result>> liveSearch(Stream<String> queries) => queries
    .debounceTime(const Duration(milliseconds: 300))  // wait for typing pause
    .distinct()                                       // ignore repeats
    .where((q) => q.length >= 2)                      // skip short queries
    .asyncMap(api.search);                            // call API

This is why streams shine for input — chaining transforms is far cleaner than orchestrating Futures by hand.


When to reach for a Stream vs a Future

SituationPick
One-shot async value (HTTP GET, DB read)Future
Data arrives over time (sockets, sensors, file chunks)Stream
Reacting to user input (search, scroll)Stream
State management (BLoC, Riverpod async)Stream
One value but pushed asynchronously to many listenersFuture + a broadcast cache

Common mistakes to avoid

// ❌ Forgetting to cancel subscriptions → memory leak
class _MyState extends State<MyWidget> {
  @override
  void initState() {
    super.initState();
    stream.listen(handleData);              // never cancelled!
  }
}

// ✅ Hold the subscription and cancel in dispose()
class _MyState extends State<MyWidget> {
  late final StreamSubscription _sub;

  @override
  void initState() {
    super.initState();
    _sub = stream.listen(handleData);
  }

  @override
  void dispose() {
    _sub.cancel();
    super.dispose();
  }
}

// ❌ Listening twice on a single-subscription stream → StateError
// ✅ Use .asBroadcastStream() or a broadcast controller

// ❌ Forgetting to close a StreamController → leaks + onDone never fires
//    Always call controller.close() (often in dispose())

// ❌ Using a raw Stream in the UI when StreamBuilder would do
//    setState on every event = manual subscription bookkeeping

Interview follow-ups

  1. When would you use a Stream instead of a Future? Any time data arrives over time — WebSocket messages, user input, sensor data, real-time DB updates (Firestore), or in state management where the UI reacts to state changes.

  2. What's the difference between Stream.listen() and await for? listen() is non-blocking, supports onError/onDone/cancelOnError, and returns a cancellable subscription. await for blocks the enclosing async function until the stream closes — clean but you can't easily cancel mid-loop or run other work concurrently.

  3. Single-subscription vs broadcast — when does it matter? Single-subscription buffers events until the (one) listener attaches; great for one-time consumers like HTTP chunks. Broadcast doesn't buffer; late listeners miss earlier events — use it for UI events, app-wide channels, etc.

  4. What is async* and how is it different from async? async returns a Future<T>. async* returns a Stream<T> and uses yield (one value) or yield* (delegate to another stream) to emit. It's the cleanest way to write a custom stream.

How helpful was this content?

Please sign in to rate this article.