StreamNotifierProvider
This is Part 6 of Mastering Riverpod: Provider Types — the final cell of the provider grid. StreamProvider gave us a read-only stream. AsyncNotifier gave us mutable async state. StreamNotifierProvider combines them: state that comes from a Stream and exposes methods to act on it. Think a chat room that streams incoming messages and lets you send one.
This is the least-used of the six (many apps never need it), but understanding it completes your mastery of the whole family — and it's the right tool for "live feed + actions."
The shape: build() returns a Stream, plus methods
A StreamNotifier mirrors AsyncNotifier, except build() returns a Stream instead of a Future:
import 'package:flutter_riverpod/flutter_riverpod.dart';
class ChatController extends StreamNotifier<List<Message>> {
@override
Stream<List<Message>> build() {
// The live source — a stream of message lists.
final roomId = ref.watch(activeRoomProvider);
return ref.watch(chatRepositoryProvider).messageStream(roomId);
}
// A mutation that acts alongside the stream:
Future<void> sendMessage(String text) async {
await ref.read(chatRepositoryProvider).send(text);
// No need to manually add to state — the stream will emit the new message.
}
}
final chatProvider =
StreamNotifierProvider<ChatController, List<Message>>(ChatController.new);
Key points:
- Extend
StreamNotifier<T>;build()returns aStream<T>. - The exposed state is an
AsyncValue<T>that updates on every stream event — same asStreamProvider. Render with.when/switch. StreamNotifierProvider<Controller, T>(Controller.new)— the same two-type-arg pattern.- Riverpod manages the subscription (subscribe on watch, cancel on dispose) — no leaks.
- You add methods (like
sendMessage) that perform actions; the stream typically reflects the change on its next emission.
So it's StreamProvider (Part 3) with mutation methods bolted on — exactly the relationship AsyncNotifier has to FutureProvider.
Reading it: same AsyncValue, plus .notifier for methods
Reading combines what you know from StreamProvider and NotifierProvider:
class ChatScreen extends ConsumerWidget {
const ChatScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final messagesAsync = ref.watch(chatProvider); // AsyncValue<List<Message>>
return Column(children: [
Expanded(
child: messagesAsync.when(
loading: () => const CircularProgressIndicator(),
error: (e, _) => Text('Error: $e'),
data: (messages) => MessageList(messages), // updates live per event
),
),
MessageInput(
onSend: (text) =>
ref.read(chatProvider.notifier).sendMessage(text), // method via .notifier
),
]);
}
}
ref.watch(chatProvider)→ the liveAsyncValue(rebuilds on each message).ref.read(chatProvider.notifier).sendMessage(...)→ call a method (in a callback).
Same watch-for-state / read(.notifier)-for-methods rule from Part 4.
Why not just StreamProvider + a separate action?
You could use a read-only StreamProvider for the feed and put sendMessage on a separate repository call in the widget. That works for simple cases. StreamNotifierProvider is the right choice when the stream and the actions belong together as one cohesive piece of state/logic:
- the controller encapsulates both the live source and the operations on it,
- methods can read the current streamed value to make decisions,
- it's a single testable unit (one controller, mockable repository),
- and consumers get one provider for "the chat," not a stream here and an action there.
Rule of thumb: if the live data and the mutations are the same feature (a chat, a collaborative document, a live cart synced over a socket), model them as one
StreamNotifier. If you only ever read the stream, keep it a simpleStreamProvider.
The complete grid, filled in
With this part, you've met all six provider types. Here's the whole map, now with everything you know:
| | Read-only | Mutable (methods) |
| --- | --- | --- |
| Sync | Provider — derived values, DI (P1) | NotifierProvider — sync mutable state (P4) |
| Future | FutureProvider — fetch-and-show (P2) | AsyncNotifierProvider — async CRUD (P5) |
| Stream | StreamProvider — live read-only (P3) | StreamNotifierProvider — live + actions (this part) |
Notice the symmetry: the right column is the left column plus methods, and each row changes only the value kind (sync → Future → Stream). Internalize this table and you'll always pick the right provider on the first try.
Practice Challenges
Challenge 1 — Stream build. Write a StreamNotifier<int> whose build() emits a counter every second.
Show solution
class Ticker extends StreamNotifier<int> {
@override
Stream<int> build() => Stream.periodic(const Duration(seconds: 1), (i) => i);
}
final tickerProvider = StreamNotifierProvider<Ticker, int>(Ticker.new);
Challenge 2 — Add a method. Add a reset() action concept — explain why mutating a live stream's state differs from a Notifier.
Show solution
In a StreamNotifier, the source of truth is the stream — methods usually perform an action (e.g. tell the backend), and the stream re-emits the new state. Unlike a sync Notifier where you set state = directly, here you typically let the stream drive state. (You can set state = AsyncData(...) for optimistic updates, but the stream remains the authority.)
Challenge 3 — Render + send. Show the chat list and wire a send button.
Show solution
final async = ref.watch(chatProvider);
// list: async.when(data: (m) => MessageList(m), loading: ..., error: ...)
onSend: (t) => ref.read(chatProvider.notifier).sendMessage(t),
Challenge 4 — Choose the cell. Stream-only price feed vs a live collaborative doc you also edit — which providers?
Show solution
Price feed (read-only) → StreamProvider. Collaborative doc (streams updates and you edit it) → StreamNotifierProvider (stream + mutation methods).
Challenge 5 — Fill the grid. Name the provider for: async + mutable; sync + read-only; stream + mutable.
Show solution
AsyncNotifierProvider; Provider; StreamNotifierProvider.
Questions to test yourself
Q1 (basic). What does a StreamNotifier's build() return, and what does the provider expose?
Show answer
build() returns a Stream<T>; the provider exposes the latest value as an AsyncValue<T> that updates on every stream event (and offers methods to act on the state).
Q2 (basic). How is StreamNotifierProvider related to StreamProvider?
Show answer
It's StreamProvider plus mutation methods — a read-only live stream becomes a mutable one with actions, just as AsyncNotifier is FutureProvider plus methods.
Q3 (intermediate). How do you read the live state vs call a method?
Show answer
ref.watch(provider) for the live AsyncValue (rebuilds per event); ref.read(provider.notifier).someMethod() to call a method (in a callback). Same watch-state / read(.notifier)-methods rule.
Q4 (intermediate). When should you choose StreamNotifierProvider over a StreamProvider plus a separate action?
Show answer
When the live data and the actions are one feature (chat, collaborative doc, socket-synced cart) — so the controller encapsulates both, methods can read the current streamed value, and consumers get a single testable provider. If you only read the stream, a plain StreamProvider is simpler.
Q5 (intermediate). Who manages the stream subscription in a StreamNotifier?
Show answer
Riverpod — it subscribes when the provider is first watched and cancels automatically on dispose, just like StreamProvider. No manual StreamSubscription/dispose.
Q6 (advanced). Describe the symmetry of the full provider grid in one sentence.
Show answer
The right (mutable) column is the left (read-only) column plus methods (Notifier/AsyncNotifier/StreamNotifier), and each row only changes the value kind — sync (Provider), Future (FutureProvider), or Stream (StreamProvider) — so the six types are just two axes: value kind × mutability.
Wrapping up
StreamNotifierProvider completes the grid:
- A
StreamNotifier<T>has abuild()returning aStream<T>and exposes state as a liveAsyncValue<T>, plus methods to act on it. - It's
StreamProvider+ mutations — for features where the live feed and the actions belong together (chat, collaborative docs). - Read state with
ref.watch, call methods viaref.read(.notifier); Riverpod manages the subscription. - The full grid is two axes: value kind (sync/Future/Stream) × mutability (read-only/methods).
There's one more group of providers you'll encounter — not new powers, but old ones you should recognize and migrate away from. Part 7 covers the legacy providers (StateProvider, StateNotifierProvider) — recognize, migrate, move on.