От Provider к Riverpod: миграция устаревшего состояния
Преобразуйте существующий код ChangeNotifier и Provider в безопасный при компиляции граф провайдеров Riverpod.
«От Provider к Riverpod: миграция устаревшего состояния» — бесплатный урок Flutter Mobile Development на CoddyKit. Это урок 1 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Flutter Mobile Development, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Flutter Mobile Development содержит 4 уроков всего.
Части этого урока еще не переведены и отображаются на английском.
Why Migrate to Riverpod?
The classic provider package solved dependency injection in Flutter, but it has real weaknesses you have probably hit in production:
- Runtime crashes — calling
context.read<T>()for a type that was never provided throws aProviderNotFoundExceptionat runtime, not compile time. - BuildContext coupling — you can only read providers where you have a
BuildContext. - No combining — depending on one
ChangeNotifierfrom another is awkward and error-prone.
Riverpod is a rewrite by the same author. Providers are top-level globals that the compiler can verify, so a missing dependency becomes a compile error. This lesson walks you through migrating a legacy ChangeNotifier + Provider app to Riverpod 2.0, piece by piece.
The Legacy Code We Are Migrating
Here is a typical legacy counter feature using ChangeNotifier. It exposes a value and a method that calls notifyListeners(). This is the pattern we will convert step by step.
Notice that the state (_count) and the mutation logic live together inside a class that extends ChangeNotifier.
// LEGACY — provider package
import 'package:flutter/foundation.dart';
class CounterModel extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void reset() {
_count = 0;
notifyListeners();
}
}How the Legacy Model Was Wired Up
In the old setup you register the model with ChangeNotifierProvider high in the widget tree, then read it via context.watch / context.read. The problem: if you forget to register it, the app compiles fine and crashes only when that screen opens.
// LEGACY wiring
void main() {
runApp(
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const MyApp(),
),
);
}
// In a widget:
final count = context.watch<CounterModel>().count;
context.read<CounterModel>().increment();Step 1 — Install Riverpod and Add ProviderScope
Add flutter_riverpod to pubspec.yaml. The single most important wiring change is to wrap your app in a ProviderScope. This is the container that stores the state of every provider — it replaces the nest of ChangeNotifierProvider widgets at the root.
- Old: many provider widgets wrapping
MyApp. - New: one
ProviderScope. Providers themselves are declared as globals, not in the tree.
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(
const ProviderScope(
child: MyApp(),
),
);
}Step 2 — ChangeNotifier becomes Notifier
Riverpod 2.0 introduces Notifier as the modern, type-safe replacement for ChangeNotifier. The differences:
- You hold state in a single
statefield instead of private fields + getters. - You never call
notifyListeners()— reassigningstaterebuilds listeners automatically. - The
build()method returns the initial state.
Here is the migrated counter as a Notifier<int>:
import 'package:flutter_riverpod/flutter_riverpod.dart';
class CounterNotifier extends Notifier<int> {
@override
int build() => 0; // initial state
void increment() => state = state + 1;
void reset() => state = 0;
}
final counterProvider = NotifierProvider<CounterNotifier, int>(
CounterNotifier.new,
);Step 3 — Read State in the UI with ConsumerWidget
Replace context.watch/context.read with a WidgetRef. The cleanest path is to make your widget a ConsumerWidget, which adds a ref parameter to build.
ref.watch(provider)— subscribe and rebuild on change (use inbuild).ref.read(provider.notifier)— get the notifier to call methods (use in callbacks).
class CounterScreen extends ConsumerWidget {
const CounterScreen({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Scaffold(
body: Center(child: Text('Count: $count')),
floatingActionButton: FloatingActionButton(
onPressed: () => ref.read(counterProvider.notifier).increment(),
child: const Icon(Icons.add),
),
);
}
}watch vs read — The Migration Trap
The most common migration bug is using the wrong access method. Map the old API to the new one carefully:
context.watch<T>()→ref.watch(provider)(rebuilds the UI)context.read<T>()in a callback →ref.read(provider.notifier)(no rebuild)
Rule: never call ref.watch inside an onPressed callback — it will not behave as expected and can cause unnecessary rebuilds. Use ref.read for one-off actions, ref.watch for values displayed in the UI.
Step 4 — Migrating Async State (FutureProvider)
Legacy apps often load data inside a ChangeNotifier with manual isLoading/error booleans. Riverpod replaces all of that with AsyncValue and a FutureProvider — loading and error states are modeled for you.
The UI then uses AsyncValue.when to render data, loading, and error branches exhaustively.
final userProvider = FutureProvider<User>((ref) async {
final repo = ref.watch(userRepositoryProvider);
return repo.fetchCurrentUser();
});
// In a ConsumerWidget:
final asyncUser = ref.watch(userProvider);
return asyncUser.when(
data: (user) => Text(user.name),
loading: () => const CircularProgressIndicator(),
error: (e, st) => Text('Error: $e'),
);Step 5 — Combining Providers (No More ProxyProvider)
In the old package, making one model depend on another required ProxyProvider and careful ordering. In Riverpod, a provider simply calls ref.watch on another provider inside its body. Dependencies are explicit, type-safe, and reactive.
Below, a derived provider recomputes automatically whenever cartProvider changes — no manual subscription, no notifyListeners chains.
final cartProvider =
NotifierProvider<CartNotifier, List<Item>>(CartNotifier.new);
// Derived state — recomputes when the cart changes
final cartTotalProvider = Provider<double>((ref) {
final items = ref.watch(cartProvider);
return items.fold(0.0, (sum, item) => sum + item.price);
});A Standalone Taste of Reactive Derivation in Dart
You do not need Flutter to understand Riverpod's core idea: derived values recompute from their inputs. This pure-Dart snippet models the same fold-the-cart logic used in cartTotalProvider, so you can run and verify the math an online judge would compute.
class Item {
final String name;
final double price;
Item(this.name, this.price);
}
double cartTotal(List<Item> items) =>
items.fold(0.0, (sum, item) => sum + item.price);
void main() {
final cart = [
Item('Coffee', 3.50),
Item('Bagel', 2.25),
Item('Juice', 4.00),
];
print('Items: ${cart.length}');
print('Total: \$${cartTotal(cart).toStringAsFixed(2)}');
}Step 6 — Incremental Migration Strategy
You rarely rewrite a whole app at once. A safe, incremental plan:
- Wrap once: add
ProviderScopeat the root immediately — it coexists with the oldproviderpackage. - Leaf-first: migrate self-contained features (settings, theme, counters) before tangled ones.
- Bridge if needed: a Riverpod provider can read legacy data, and a legacy widget can stay until its screen is converted.
- Convert widgets: change
StatelessWidget→ConsumerWidgetandStatefulWidget→ConsumerStatefulWidgetas you touch each screen. - Delete the old
ChangeNotifierProviderand theproviderdependency only when nothing references them.
Quick Check — watch vs read
Test your understanding of the most error-prone part of the migration.
Recap — From Provider to Riverpod
You migrated a legacy ChangeNotifier + Provider feature to Riverpod 2.0:
- Wrapped the app in a single ProviderScope instead of nested provider widgets.
- Turned
ChangeNotifierinto a Notifier with astatefield and nonotifyListeners(). - Swapped
context.watch/readforref.watch(display) andref.read(provider.notifier)(actions) inside a ConsumerWidget. - Modeled async with FutureProvider +
AsyncValue.when, and combined providers viaref.watchinstead ofProxyProvider. - Followed a leaf-first, incremental path so the two systems coexist during the transition.
The payoff: a compile-safe provider graph where a missing dependency is a build error, not a production crash.
Изучай Dart с ИИ-репетитором — бесплатно
Пиши и запускай код прямо в браузере, получай мгновенную помощь от ИИ-репетитора 24/7 и продолжи учиться на сайте или в приложении.
- Курсы
- 22
- Уроки
- 88
Часто задаваемые вопросы
Урок «От Provider к Riverpod: миграция устаревшего состояния» бесплатный?
Да — полный текст урока «От Provider к Riverpod: миграция устаревшего состояния» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Flutter Mobile Development, подпишись на CoddyKit PRO. Курс Flutter Mobile Development содержит 4 уроков всего.
Чему я научусь в уроке «От Provider к Riverpod: миграция устаревшего состояния»?
Преобразуйте существующий код ChangeNotifier и Provider в безопасный при компиляции граф провайдеров Riverpod. Ты практикуешь Flutter Mobile Development с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Flutter Mobile Development?
Предыдущий опыт не требуется. Flutter Mobile Development на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 1 из 4.
Сколько времени занимает урок «От Provider к Riverpod: миграция устаревшего состояния»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Flutter Mobile Development?
Да. Каждый урок Flutter Mobile Development включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- От Provider к Riverpod: миграция устаревшего состояния
- Генерация кода с riverpod_generator и @riverpod
- Конвейеры данных AsyncNotifier и FutureProvider
- Области действия, переопределения и ProviderObserver