Migrating from Provider to Riverpod
The Riverpod docs have a dedicated section for coming from Provider: motivation, a quickstart and an API mapping. The key mappings:
BuildContext.watch→WidgetRef.watchBuildContext.read→WidgetRef.readBuildContext.select→WidgetRef.watch(myProvider.select(...))ProxyProvider→ref.watch()inside a providerChangeNotifierProxyProvider→ref.listen()MultiProvider→ProviderScopeat the root, with providers as globalfinalvariablesChangeNotifier→Notifier/AsyncNotifier
The docs put the most important change this way: Notifier/AsyncNotifier combined with immutable state leads to better design choices and fewer errors. With AsyncNotifier there are no isLoading/hasError flags to maintain — the future returned from build becomes an AsyncValue automatically.
// ---------- ƏVVƏL: ChangeNotifier ----------
class TodoNotifier extends ChangeNotifier {
TodoNotifier(this._repository) {
load(); // konstruktorda yükləmə
}
final TodoRepository _repository;
List<Todo> todos = [];
bool isLoading = true; // əl ilə saxlanan bayraq
bool hasError = false; // əl ilə saxlanan bayraq
Future<void> load() async {
isLoading = true;
hasError = false;
notifyListeners();
try {
todos = await _repository.fetchAll();
} catch (_) {
hasError = true;
} finally {
isLoading = false;
notifyListeners();
}
}
}
final todoProvider = ChangeNotifierProvider<TodoNotifier>((ref) {
return TodoNotifier(ref.watch(todoRepositoryProvider));
});
// ---------- SONRA: AsyncNotifier ----------
class Todos extends AsyncNotifier<List<Todo>> {
@override
Future<List<Todo>> build() {
// Yükləmə məntiqi build-də: future qaytarmaq kifayətdir.
return ref.watch(todoRepositoryProvider).fetchAll();
}
Future<void> reload() async {
state = const AsyncLoading();
state = await AsyncValue.guard(
() => ref.read(todoRepositoryProvider).fetchAll(),
);
}
}
final todosProvider =
AsyncNotifierProvider<Todos, List<Todo>>(Todos.new);
// UI: isLoading/hasError yerinə pattern matching.
// switch (ref.watch(todosProvider)) { ... }Before / after: ChangeNotifier → AsyncNotifier (the structure from the official migration doc).
| Provider | Riverpod | Watch out |
|---|---|---|
| `MultiProvider` at the root | `ProviderScope` at the root | Providers move from the tree to global `final` variables |
| `ChangeNotifierProvider` | `NotifierProvider` / `AsyncNotifierProvider` | State becomes immutable; `notifyListeners()` disappears |
| `ProxyProvider` | `ref.watch()` inside a provider | The dependency graph becomes visible in the code |
| `context.select` | `ref.watch(p.select(...))` | The selected value must be immutable |
| Rebuilding the tree by hand in widget tests | `ProviderScope(overrides: [...])` | Test code shrinks |
A practical migration strategy: the two packages can live side by side in one project. You can nest ProviderScope inside MultiProvider (or the other way round) and move features one at a time. That is far safer than "migrate everything in one sprint" — each migrated feature arrives as its own PR with its own tests. In Riverpod 3 ChangeNotifierProvider still exists (in the legacy import), so ChangeNotifiers can be kept for a while during the transition.
📚 Sources and documentation
- Provider vs Riverpodofficialriverpod.dev
The official source of the API mapping table.
- Coming from Provider: motivationofficialriverpod.dev
- Coming from Provider: quickstartofficialriverpod.dev
- Migrating from ChangeNotifierofficialriverpod.dev
Replacing isLoading/hasError with AsyncValue — before/after code.