With Riverpod 3, how does StreamProvider expose a live unread-notifications count, and what does a widget get from ref.watch on it?
answer
- AsyncValue of the latest event
- loading until the first event
- one subscription for all watchers
- paused when nobody listens
- autoDispose for real resources
basics
~20 sStreamProvider subscribes to the stream its function returns and exposes the latest event as AsyncValue<int>: loading until the first event, then data, or error if the stream emits one. All widgets watching it share that subscription.
solid answer
~40 s`StreamProvider<int>((ref) => repository.watchUnreadCount())` calls the function when first read, listens to the returned stream and stores its latest event. Watching it gives `AsyncValue<int>`: `AsyncLoading` until the first event, `AsyncData(count)` afterwards, `AsyncError` if the stream emits an error. Every widget watching the provider reads the same cached value from one subscription, unlike a `StreamBuilder` per widget. Since Riverpod 3.0 the subscription is paused while the provider is not actively listened to. The function re-runs, with a new stream, when a provider it watches changes. Riverpod's documentation warns that a plain `StreamProvider` is almost never destroyed, so for sockets or database watchers use the auto-dispose variant and close resources in `ref.onDispose`. Add `StreamNotifierProvider` when the stream must also expose methods.
code
dart · 17 linesimport 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class UnreadBadge extends ConsumerWidget {
const UnreadBadge({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final unread = ref.watch(unreadCountProvider);
return switch (unread) {
AsyncData(:final value) when value > 0 =>
Badge.count(count: value, child: const Icon(Icons.notifications)),
AsyncError() => const Icon(Icons.notifications_off),
_ => const Icon(Icons.notifications),
};
}
}go deeper
Recall that StreamProvider exposes a stream's latest event as AsyncValue, loading until the first event arrives.
Explain the shared subscription, the cached latest value, re-running when a watched provider changes, and how it differs from StreamBuilder.
Manage subscription lifetime: pausing, auto-dispose for real resources, cleanup in onDispose, and StreamNotifierProvider when actions are needed.
Decide which real-time sources live in providers, how their connections are pooled or disposed, and what that means for battery and data use.
## What StreamProvider does In **Riverpod 3**, **`StreamProvider<T>`** is the functional provider for values that keep arriving: a WebSocket, a database query that re-emits on change, a platform event channel. Its source documentation describes it as identical in behaviour and usage to `FutureProvider`, except that the created value is a `Stream` instead of a `Future`. For a messaging app's unread badge: ```dart final unreadCountProvider = StreamProvider<int>((ref) { return ref.watch(chatRepositoryProvider).watchUnreadCount(); }); ``` ## What a widget receives `ref.watch(unreadCountProvider)` returns **`AsyncValue<int>`**, never a raw `int` or a `Stream`: 1. Before the stream's first event: `AsyncLoading`. 2. After each event: `AsyncData` with that event's value. 3. If the stream emits an error: `AsyncError` with the error and stack trace. Widgets typically switch over it and show a badge only for data. Every emission that changes the value rebuilds watchers; since Riverpod 3.0 all providers filter notifications with `==`. ## Why not StreamBuilder A `StreamBuilder` subscribes per widget and needs the same stream instance across rebuilds to avoid resubscribing. `StreamProvider` moves the subscription out of the widget tree: - **one subscription** per provider in the container, however many widgets watch it (the app bar badge and the inbox tab share it); - **a cached latest value**, so a widget that starts watching later immediately gets the current count; - **reactive recreation**: if the function `ref.watch`es another provider (the signed-in user), a change there re-runs the function and subscribes to the new stream; - **composability**: other providers can depend on it, and `unreadCountProvider.future` gives a `Future` for code that needs to await a value. ## Lifetime of the subscription Two lifecycle rules matter in production: - **Pausing.** Riverpod 3.0 made `StreamProvider` pause its `StreamSubscription` when the provider is not actively listened to, and resume it when a listener returns. - **Disposal.** The source documentation warns that a plain `StreamProvider` is almost never destroyed, so resources such as sockets stay open. For anything that holds a connection, declare the auto-dispose variant and close the resource in `ref.onDispose`. | Concern | Plain `StreamProvider` | Auto-dispose variant | |---|---|---| | Subscription while unwatched | paused | paused, then disposed | | Socket or listener cleanup | rarely - the provider is almost never destroyed | when the last listener leaves | | Latest value when re-watched | kept | recomputed from a new stream | ## When a class is needed `StreamProvider` is read-only. If the badge also needs actions - "mark all as read" that should reset the count immediately - Riverpod offers **`StreamNotifierProvider`**, whose `StreamNotifier` returns the stream from `build()` and exposes methods that assign `state`. ## Pitfalls - Watching the provider and treating the result as `int` - it is an `AsyncValue`. - Creating the stream inside a widget's `build` and passing it to the provider through global state - let the provider create it. - Holding a socket in a plain `StreamProvider` with no cleanup. - Expecting the stream to keep running for a screen nobody is looking at - it is paused.
- The unread count depends on the signed-in user. How does StreamProvider switch streams when the user changes?Watch the user provider inside the StreamProvider's function. When it changes, Riverpod re-runs the function, which returns the new user's stream, and the provider subscribes to it. Widgets keep watching the same provider and receive the new user's counts.
- Why does Riverpod's documentation recommend the auto-dispose variant for a socket-backed StreamProvider?A plain `StreamProvider` is almost never destroyed, so its socket stays open even when no screen shows the data. The auto-dispose variant is disposed when its last listener goes away, and a `ref.onDispose` callback can close the socket then.
saying these in an interview costs you the question
- Treats ref.watch on a StreamProvider as returning the latest int directly.
- Believes each watching widget opens its own stream subscription.
- Expects the subscription to keep running at full speed while nobody watches.
- Holds a WebSocket in a plain StreamProvider without any cleanup.
- Uses StreamProvider and then looks for a setter to reset the count.