Sparround

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.watchWidgetRef.watch
  • BuildContext.readWidgetRef.read
  • BuildContext.selectWidgetRef.watch(myProvider.select(...))
  • ProxyProviderref.watch() inside a provider
  • ChangeNotifierProxyProviderref.listen()
  • MultiProviderProviderScope at the root, with providers as global final variables
  • ChangeNotifierNotifier / 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.

dart
// ---------- Ə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).

ProviderRiverpodWatch out
`MultiProvider` at the root`ProviderScope` at the rootProviders move from the tree to global `final` variables
`ChangeNotifierProvider``NotifierProvider` / `AsyncNotifierProvider`State becomes immutable; `notifyListeners()` disappears
`ProxyProvider``ref.watch()` inside a providerThe 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