Legacy Providers
This is Part 7 of Mastering Riverpod: Provider Types — and it's a bit different. The six providers in the grid are everything you need for new code. But Riverpod has been around for years, so you will encounter older provider types in existing codebases, Stack Overflow answers, and pre-3.0 tutorials. This part's job is simple: recognize them, understand why they're legacy, and know how to migrate to the modern API — then move on.
In Riverpod 3.0 these are officially legacy:
StateProvider,StateNotifierProvider, andChangeNotifierProviderwere moved to a separatelegacyimport path and discouraged for new code (not removed — your old code still works).
The legacy lineup
Three providers are now legacy. Here's what each was, and what replaces it:
| Legacy provider | Was used for | Modern replacement |
| --- | --- | --- |
| StateProvider | a single simple mutable value (a bool, an int, a filter) | NotifierProvider (Part 4) |
| StateNotifierProvider | complex mutable state via a StateNotifier class | NotifierProvider / AsyncNotifierProvider (Parts 4–5) |
| ChangeNotifierProvider | bridging Flutter's ChangeNotifier (mutable, listener-based) | NotifierProvider |
To even use them in 3.0 you import the legacy path explicitly, which is itself a signal:
import 'package:flutter_riverpod/legacy.dart'; // legacy providers live here now
StateProvider — and why it's gone
StateProvider was the quick way to expose a single mutable value without writing a class:
// LEGACY — recognize it, don't write it.
final filterProvider = StateProvider<Filter>((ref) => Filter.all);
// Read: ref.watch(filterProvider) → Filter
// Write: ref.read(filterProvider.notifier).state = Filter.active;
It was convenient but had real problems: mutation logic leaked into widgets (you set .notifier.state = directly from the UI), there was no natural place for related methods, and it didn't scale past trivial state. The modern Notifier is barely more code and far cleaner.
Migrating StateProvider → NotifierProvider
// MODERN — a tiny Notifier with named methods.
class FilterController extends Notifier<Filter> {
@override
Filter build() => Filter.all;
void set(Filter f) => state = f; // intent-revealing methods
void showAll() => state = Filter.all;
}
final filterProvider = NotifierProvider<FilterController, Filter>(FilterController.new);
// Read: ref.watch(filterProvider) → Filter (unchanged for consumers!)
// Write: ref.read(filterProvider.notifier).set(Filter.active);
Note: reading is identical (ref.watch(filterProvider)), so consumers barely change. You gain named methods (set, showAll) instead of raw .state = assignments scattered across widgets — mutation logic moves into the controller where it belongs.
StateNotifierProvider — the bigger migration
StateNotifierProvider paired with a StateNotifier<T> class was the previous recommended way to manage complex state. It's the closest in spirit to today's Notifier, but with clunkier ergonomics (you passed initial state to super, ref wasn't a property, etc.):
// LEGACY StateNotifier
class CounterSN extends StateNotifier<int> {
CounterSN() : super(0); // initial state via super()
void increment() => state++;
}
final counterProvider = StateNotifierProvider<CounterSN, int>((ref) => CounterSN());
Migrating StateNotifier → Notifier
The mapping is mechanical:
// MODERN Notifier
class Counter extends Notifier<int> {
@override
int build() => 0; // initial state via build() instead of super()
void increment() => state++; // methods are unchanged
}
final counterProvider = NotifierProvider<Counter, int>(Counter.new);
The migration rules:
StateNotifier<T>→Notifier<T>; constructorsuper(initial)→build()returning the initial state.StateNotifierProvider<N, T>((ref) => N())→NotifierProvider<N, T>(N.new).- Inside the class,
refis now a property (inStateNotifieryou had to pass it in) — so reading other providers is cleaner. - Async equivalent: a
StateNotifierdoing async work → anAsyncNotifier(Part 5). - Methods (
increment, etc.) carry over unchanged.
ChangeNotifierProvider — the most discouraged
ChangeNotifierProvider bridged Flutter's ChangeNotifier (the notifyListeners() mutable-object pattern, also used by the provider package). It was always a compatibility shim, and it's the one to migrate most eagerly because ChangeNotifier encourages mutable state with manual notifyListeners() — the opposite of Riverpod's immutable-state model (Part 4).
Replace it with a Notifier holding immutable state and assigning state = ... instead of mutating fields and calling notifyListeners().
Why the move to Notifier? (The rationale)
Riverpod 3.0 consolidated on the Notifier family for good reasons:
- One consistent model —
Notifier/AsyncNotifier/StreamNotifiermirror the read-onlyProvider/FutureProvider/StreamProvider, so the whole grid is uniform. refas a property — cleaner access to other providers from inside the class.- Better code-gen support — the
@riverpodannotation (a later series) generatesNotifier-based code. - Immutable-state discipline —
Notifier'sstate =model steers you away from the in-place mutation thatChangeNotifierinvites. - Unified auto-dispose (Foundation Part 1) — fewer duplicated classes.
You don't have to rewrite a working app overnight — legacy providers still function. But for new code, always reach for the Notifier family, and migrate legacy providers opportunistically when you touch that code.
A migration cheat-sheet
StateProvider<T> → NotifierProvider<C, T> (C extends Notifier<T>)
StateNotifier<T> + provider → Notifier<T> + NotifierProvider (super(x) → build())
StateNotifier (async) → AsyncNotifier<T> + AsyncNotifierProvider
ChangeNotifierProvider → NotifierProvider with immutable state (no notifyListeners)
import '.../legacy.dart' → import '.../flutter_riverpod.dart' (drop the legacy import)
Practice Challenges
Challenge 1 — Recognize it. Is StateProvider<bool>((ref) => false) modern or legacy, and what replaces it?
Show solution
Legacy. Replace with a NotifierProvider whose Notifier<bool> exposes a toggle()/set() method instead of writing .notifier.state = from widgets.
Challenge 2 — Migrate a StateProvider. Convert final countP = StateProvider<int>((ref) => 0); to modern form with an increment().
Show solution
class CountC extends Notifier<int> {
@override
int build() => 0;
void increment() => state++;
}
final countP = NotifierProvider<CountC, int>(CountC.new);
Consumers still ref.watch(countP); writes become ref.read(countP.notifier).increment().
Challenge 3 — Migrate a StateNotifier. Convert this:
class TodosSN extends StateNotifier<List<Todo>> {
TodosSN() : super([]);
void add(Todo t) => state = [...state, t];
}
final p = StateNotifierProvider<TodosSN, List<Todo>>((ref) => TodosSN());
Show solution
class Todos extends Notifier<List<Todo>> {
@override
List<Todo> build() => [];
void add(Todo t) => state = [...state, t];
}
final p = NotifierProvider<Todos, List<Todo>>(Todos.new);
super([]) → build() => []; provider uses Todos.new; the method is unchanged.
Challenge 4 — Spot the import. You see import 'package:flutter_riverpod/legacy.dart';. What does it tell you?
Show solution
The file uses legacy providers (StateProvider/StateNotifierProvider/ChangeNotifierProvider), which moved to the legacy import in 3.0. It's a flag to migrate this code to the Notifier family and drop the legacy import.
Challenge 5 — ChangeNotifier red flag. Why migrate ChangeNotifierProvider most eagerly?
Show solution
ChangeNotifier relies on mutating fields in place and calling notifyListeners() — the opposite of Riverpod's immutable state = model (Part 4). Replacing it with a Notifier holding immutable state gives you predictable update-filtering (==) and aligns with the rest of Riverpod.
Questions to test yourself
Q1 (basic). Which three providers are legacy in Riverpod 3.0?
Show answer
StateProvider, StateNotifierProvider, and ChangeNotifierProvider — moved to the legacy import and discouraged for new code (still functional).
Q2 (basic). What is StateProvider replaced by?
Show answer
NotifierProvider (with a small Notifier class exposing named mutation methods instead of writing .notifier.state = from widgets).
Q3 (intermediate). When migrating StateNotifier to Notifier, what replaces super(initialState)?
Show answer
Overriding build() to return the initial state. (StateNotifierProvider<N,T>((ref)=>N()) also becomes NotifierProvider<N,T>(N.new), and methods carry over unchanged.)
Q4 (intermediate). Do consumers change much when you migrate StateProvider → NotifierProvider?
Show answer
Not for reading — ref.watch(provider) is unchanged. Writes change from ref.read(provider.notifier).state = x to calling a named method like ref.read(provider.notifier).set(x). So consumer impact is minimal and mostly improves clarity.
Q5 (intermediate). What's the async equivalent when migrating a StateNotifier that did network work?
Show answer
An AsyncNotifier (Part 5) — async build(), state as AsyncValue, mutations via state = AsyncLoading() → AsyncValue.guard(...).
Q6 (advanced). Give two reasons Riverpod 3.0 consolidated on the Notifier family.
Show answer
Any two of: (1) a uniform model mirroring the read-only providers (Notifier/AsyncNotifier/StreamNotifier ↔ Provider/FutureProvider/StreamProvider); (2) ref as a class property for cleaner access to dependencies; (3) better code-generation support via @riverpod; (4) immutable-state discipline (vs ChangeNotifier's notifyListeners()); (5) unified auto-dispose removing duplicated classes.
Wrapping up
Legacy providers are for recognizing, not writing:
StateProvider,StateNotifierProvider,ChangeNotifierProviderare legacy in 3.0 (moved to thelegacyimport, still functional).- Migrate them to the
Notifierfamily:StateProvider→NotifierProvider;StateNotifier→Notifier/AsyncNotifier(super(x)→build(),N.new);ChangeNotifier→Notifierwith immutable state. - Reading consumers barely change; writes become named methods — clearer and more testable.
- For new code, always use the modern grid; migrate legacy opportunistically.
That completes a tour of every provider type — read-only and mutable, sync, Future, and Stream, plus the legacy ones. Time to prove it. Part 8 is the 100-question Riverpod Provider Types mastery bank with coding mini-exercises.