← Back to blog
Mastering Riverpod: Provider Types · Part 3 of 8
August 10, 20266 min read

StreamProvider — Real-Time Data with Riverpod

RiverpodFlutterDart

StreamProvider

This is Part 3 of Mastering Riverpod: Provider Types. FutureProvider handled data that arrives once. But lots of data keeps arriving over time — chat messages, a Firestore document, live stock prices, GPS updates, connectivity changes. That's a Stream (recall the async series), and Riverpod exposes it as reactive state with StreamProvider.

The great news: if you understand FutureProvider, you already understand StreamProvider. They're twins — one wraps a Future, the other a Stream — and both speak AsyncValue.


Declaring a StreamProvider

You declare it with a function that returns a Stream:

final clockProvider = StreamProvider<DateTime>((ref) {
  return Stream.periodic(const Duration(seconds: 1), (_) => DateTime.now());
});

Like FutureProvider<T>, the type StreamProvider<T> is the element type (DateTime), not Stream<DateTime>. The provider subscribes to the stream lazily on first watch and exposes the latest event as an AsyncValue.


Same AsyncValue, but it updates per event

ref.watch(clockProvider) gives an AsyncValue<DateTime> — exactly like FutureProvider — with one beautiful difference: it updates every time the stream emits. So .when re-runs and your UI rebuilds on each event:

class ClockScreen extends ConsumerWidget {
  const ClockScreen({super.key});
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final clockAsync = ref.watch(clockProvider); // AsyncValue<DateTime>

    return clockAsync.when(
      loading: () => const Text('Starting…'),          // before the first event
      error: (err, st) => Text('Stream error: $err'),  // if the stream errors
      data: (time) => Text('$time'),                   // updates every second
    );
  }
}

The state flow:

StreamProvider<DateTime>
├── AsyncLoading            — subscribed, no event yet
├── AsyncData(time)         — latest emitted value (re-emits on each event)
└── AsyncError(e, st)       — the stream emitted an error

Because it's the same AsyncValue sealed type (Part 2), the same .when / switch handling applies — you've already learned the hard part.


Subscriptions are managed for you

Here's the quiet superpower. In raw Flutter, a Stream means a StreamSubscription you must cancel in dispose or leak (recall the async series and Flutter Part 3). With StreamProvider, Riverpod owns the subscription:

  • It subscribes when the provider is first watched.
  • It caches the latest value and pushes updates to watchers.
  • It cancels the subscription automatically when the provider is disposed (no listeners / out of scope).

No StreamSubscription, no dispose, no leak. This is a big ergonomic win over StreamBuilder (which you'd re-create in build) and manual subscriptions.


Real-world patterns

StreamProvider shines anywhere a backend exposes a stream. Two canonical examples:

Firestore live document

final userDocProvider = StreamProvider<User>((ref) {
  final uid = ref.watch(currentUidProvider);
  return FirebaseFirestore.instance
      .collection('users').doc(uid).snapshots()       // a Stream of snapshots
      .map((snap) => User.fromMap(snap.data()!));
});

The UI watches userDocProvider and updates live whenever the document changes in Firestore — no manual refresh.

Connectivity / auth state

final authStateProvider = StreamProvider<User?>((ref) {
  return FirebaseAuth.instance.authStateChanges(); // emits on login/logout
});

Now the whole app can ref.watch(authStateProvider) to react to sign-in/sign-out instantly — a classic auth-gating pattern.


Combining with other providers

Just like FutureProvider, you can await a StreamProvider's first value via .future, or watch its AsyncValue. And a StreamProvider's function can ref.watch other providers, re-subscribing when they change:

final messagesProvider = StreamProvider<List<Message>>((ref) {
  final roomId = ref.watch(activeRoomProvider); // re-subscribe when room changes
  return ref.watch(chatRepoProvider).messageStream(roomId);
});

Switch rooms → activeRoomProvider changes → Riverpod disposes the old subscription and subscribes to the new room's stream. Reactive, leak-free, automatic.


StreamProvider vs FutureProvider vs the rest

| | FutureProvider | StreamProvider | | --- | --- | --- | | Source | a Future (one value) | a Stream (many values over time) | | ref.watch returns | AsyncValue<T> | AsyncValue<T> | | Updates | once (then cached) | on every stream event | | Subscription | n/a | auto-managed (subscribe/cancel) | | Mutable? | no | no |

The same limitation as FutureProvider: StreamProvider is read-only. It exposes the stream's values but offers no way to mutate state or combine a stream with user actions. When you need both a stream and methods (e.g. a chat that streams messages and lets you sendMessage()), you want a StreamNotifierProvider (Part 6).


Practice Challenges

Challenge 1 — Declare one. Write a StreamProvider<int> that emits an incrementing counter every second.

Show solution
final tickerProvider = StreamProvider<int>((ref) =>
    Stream.periodic(const Duration(seconds: 1), (i) => i));

Challenge 2 — Render it. Show tickerProvider with loading/error/data.

Show solution
ref.watch(tickerProvider).when(
  loading: () => const Text('starting'),
  error: (e, _) => Text('$e'),
  data: (n) => Text('Tick $n'), // updates each second
);

Challenge 3 — Firestore live data. Sketch a StreamProvider exposing a user document live.

Show solution
final userDocProvider = StreamProvider<User>((ref) {
  return firestore.collection('users').doc(uid).snapshots()
      .map((s) => User.fromMap(s.data()!));
});

The UI updates live as the document changes.

Challenge 4 — Re-subscribe on change. A messages stream depends on roomIdProvider. Write it so changing rooms re-subscribes.

Show solution
final messagesProvider = StreamProvider<List<Message>>((ref) {
  final roomId = ref.watch(roomIdProvider);
  return ref.watch(chatRepoProvider).messageStream(roomId);
});

Watching roomIdProvider makes Riverpod re-create the subscription when it changes.

Challenge 5 — Outgrown it. You stream chat messages but also need sendMessage(). What provider?

Show solution

A StreamNotifierProvider (Part 6) — it combines a stream with mutation methods. StreamProvider is read-only.


Questions to test yourself

Q1 (basic). What does StreamProvider wrap, and what does ref.watch return?

Show answer

It wraps a Stream<T> and ref.watch returns an AsyncValue<T> — the same loading/data/error type as FutureProvider, but it updates on every stream event.

Q2 (basic). How is StreamProvider similar to FutureProvider?

Show answer

Both expose async state as an AsyncValue and are read-only; you handle them identically with .when/switch. The difference is the source: a Future (one value) vs a Stream (many over time).

Q3 (intermediate). What does Riverpod manage for you that you'd do manually with a raw Stream?

Show answer

The StreamSubscription — Riverpod subscribes on first watch, pushes updates, and cancels automatically when the provider is disposed. No manual dispose/cancel, no leak (unlike a raw subscription or StreamBuilder).

Q4 (intermediate). When does StreamProvider show AsyncLoading?

Show answer

After it subscribes but before the first event arrives. Once the stream emits, it transitions to AsyncData; if the stream emits an error, it becomes AsyncError.

Q5 (intermediate). How do you make a StreamProvider re-subscribe when some input changes?

Show answer

ref.watch the input provider inside the stream function (e.g. ref.watch(roomIdProvider)). When it changes, Riverpod disposes the old subscription and re-runs the function to subscribe to the new stream.

Q6 (advanced). When should you choose StreamNotifierProvider over StreamProvider?

Show answer

When you need a stream and the ability to mutate/act on it — e.g. a chat feed that streams incoming messages but also exposes sendMessage(), or stream state you can imperatively update. StreamProvider is read-only; StreamNotifierProvider (Part 6) combines the stream with methods.


Wrapping up

StreamProvider is FutureProvider's real-time twin:

  • It wraps a Stream<T> and exposes the latest event as an AsyncValue<T> that updates on every emission.
  • Same .when/switch handling as FutureProvider — you already knew it.
  • Riverpod auto-manages the subscription (subscribe on watch, cancel on dispose) — no leaks, no StreamBuilder.
  • Perfect for Firestore snapshots, auth state, sockets, connectivity — and re-subscribes when watched inputs change.
  • It's read-only; combine streams with mutations using StreamNotifierProvider.

So far every provider has been read-only — fetch and display. But apps need to change state: increment a counter, toggle a setting, add a todo. For that we cross into the mutable column of the grid. Part 4 introduces the modern way to hold mutable state: NotifierProvider.