Riverpod Notifiers: StateNotifier and NotifierProvider
Riverpod Notifiers: StateNotifier and NotifierProvider
As your Riverpod-managed features grow in complexity, raw StateProvider becomes too limited — it exposes the mutable state directly and mixes business logic into the UI. StateNotifier and its modern successor Notifier solve this by encapsulating both the state and the operations that may change it inside a dedicated class, while widgets only ever receive an immutable snapshot of that state.
NotifierProvider (or StateNotifierProvider). Widgets never mutate state directly.StateNotifier — the Classic API
StateNotifier<S> from the state_notifier package (re-exported by Riverpod) is a class that:
- Holds a single immutable state of type
S. - Exposes methods to produce a new state (you replace
state, never mutate it). - Notifies listeners automatically whenever
stateis reassigned. - Is registered with
StateNotifierProviderso any widget can watch it.
StateNotifier — Shopping Cart Example
import 'package:flutter_riverpod/flutter_riverpod.dart';
// 1. Define an immutable state class
class CartState {
final List<String> items;
final bool isLoading;
const CartState({this.items = const [], this.isLoading = false});
// copyWith pattern keeps mutations safe
CartState copyWith({List<String>? items, bool? isLoading}) {
return CartState(
items: items ?? this.items,
isLoading: isLoading ?? this.isLoading,
);
}
}
// 2. Extend StateNotifier with your business logic
class CartNotifier extends StateNotifier<CartState> {
CartNotifier() : super(const CartState());
void addItem(String product) {
// Always create a NEW state — never mutate the existing list in place
state = state.copyWith(items: [...state.items, product]);
}
void removeItem(String product) {
state = state.copyWith(
items: state.items.where((i) => i != product).toList(),
);
}
Future<void> checkout() async {
state = state.copyWith(isLoading: true);
await Future.delayed(const Duration(seconds: 2)); // simulate network
state = const CartState(); // reset after checkout
}
}
// 3. Expose the notifier through a provider
final cartProvider = StateNotifierProvider<CartNotifier, CartState>(
(ref) => CartNotifier(),
);
Consuming StateNotifierProvider in the UI
Widgets use ref.watch to subscribe to the state and ref.read (inside callbacks) to call methods on the notifier. The notifier instance is accessed via the .notifier modifier.
Widget That Reads and Drives a StateNotifier
class CartScreen extends ConsumerWidget {
const CartScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
// watch() subscribes — rebuilds when CartState changes
final cart = ref.watch(cartProvider);
return Scaffold(
appBar: AppBar(title: Text('Cart (${cart.items.length})')),
body: cart.isLoading
? const Center(child: CircularProgressIndicator())
: ListView(
children: cart.items
.map((item) => ListTile(
title: Text(item),
trailing: IconButton(
icon: const Icon(Icons.remove_circle),
onPressed: () => ref
.read(cartProvider.notifier)
.removeItem(item),
),
))
.toList(),
),
floatingActionButton: FloatingActionButton(
onPressed: () =>
ref.read(cartProvider.notifier).addItem('New Product'),
child: const Icon(Icons.add),
),
);
}
}
ref.watch(provider) inside build to get reactive state. Use ref.read(provider.notifier) inside callbacks and event handlers — reading (not watching) during an event avoids accidental rebuilds triggered by the read itself.The Modern Notifier API (Riverpod 2.x)
Riverpod 2 introduced Notifier<S> and NotifierProvider as the recommended replacement for StateNotifier. Key differences:
- The notifier class extends
Notifier<S>instead ofStateNotifier<S>. - The initial state is returned from the required
build()method — making it easy for code generation (riverpod_generator) to work with it. - The notifier has direct access to
refviathis.ref, so it can read other providers without constructor injection. - Registered with
NotifierProvider<NotifierClass, StateType>.
Same Cart — Rewritten with the Notifier API
import 'package:flutter_riverpod/flutter_riverpod.dart';
// Same immutable CartState as before (reuse the copyWith pattern)
class CartNotifier2 extends Notifier<CartState> {
@override
CartState build() {
// build() replaces the constructor super() call
return const CartState();
}
void addItem(String product) {
state = state.copyWith(items: [...state.items, product]);
}
void removeItem(String product) {
state = state.copyWith(
items: state.items.where((i) => i != product).toList(),
);
}
Future<void> checkout() async {
state = state.copyWith(isLoading: true);
// Access other providers through ref without passing them in
// final user = ref.read(currentUserProvider);
await Future.delayed(const Duration(seconds: 2));
state = const CartState();
}
}
// NotifierProvider — new style
final cartProvider2 = NotifierProvider<CartNotifier2, CartState>(
CartNotifier2.new,
);
state setter in both APIs triggers a rebuild only if the new value is not identical to the old one (using ==). If your state class does not override ==, Dart compares by reference — so always create a new instance (via copyWith or a fresh constructor call) rather than mutating fields in place.Async Notifier (AsyncNotifier)
When your state involves asynchronous data (API calls, local database reads), use AsyncNotifier<S> with AsyncNotifierProvider. The build() method becomes Future<S> build(), and state is automatically wrapped in AsyncValue<S> for free loading/error/data handling.
Choosing Between StateNotifier and Notifier
- New projects: Prefer
Notifier/NotifierProvider— it is the officially recommended API in Riverpod 2.x and integrates cleanly withriverpod_generator. - Existing codebases:
StateNotifierProvidercontinues to work; there is no urgency to migrate. Both APIs are fully supported. - Async operations: Use
AsyncNotifier/AsyncNotifierProviderto get automaticAsyncValuewrapping.
Summary
StateNotifier and the newer Notifier API are the primary tools for encapsulating mutable state and business logic in Riverpod. Both follow the same pattern: an immutable state class (often with copyWith), a notifier class that exposes named business-operation methods, and a provider that wires them into the widget tree. Widgets remain pure: they watch the read-only state and invoke notifier methods — never touching state directly.
copyWith. Use StateNotifier for legacy/compatibility and Notifier for new Riverpod 2 code. Access methods via ref.read(provider.notifier) and subscribe to state via ref.watch(provider).