0Pricing
Flutter Mobile Development · 课时

从 Provider 到 Riverpod:迁移旧状态管理

将现有的 ChangeNotifier 和 Provider 代码转换为 Riverpod 的编译安全提供者图。

从 Provider 到 Riverpod:迁移旧状态管理 是 CoddyKit 上的免费 Flutter Mobile Development 课时。 这是第 1 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 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 a ProviderNotFoundException at runtime, not compile time.
  • BuildContext coupling — you can only read providers where you have a BuildContext.
  • No combining — depending on one ChangeNotifier from 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 state field instead of private fields + getters.
  • You never call notifyListeners() — reassigning state rebuilds 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 in build).
  • 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 ProviderScope at the root immediately — it coexists with the old provider package.
  • 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→ConsumerWidget and StatefulWidget→ConsumerStatefulWidget as you touch each screen.
  • Delete the old ChangeNotifierProvider and the provider dependency 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 ChangeNotifier into a Notifier with a state field and no notifyListeners().
  • Swapped context.watch/read for ref.watch (display) and ref.read(provider.notifier) (actions) inside a ConsumerWidget.
  • Modeled async with FutureProvider + AsyncValue.when, and combined providers via ref.watch instead of ProxyProvider.
  • 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.

常见问题解答

「从 Provider 到 Riverpod:迁移旧状态管理」课时是免费的吗?

是的 — 「从 Provider 到 Riverpod:迁移旧状态管理」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 Flutter Mobile Development 课程的其余内容,请升级到 CoddyKit PRO。 Flutter Mobile Development 课程共包含 4 节课。

「从 Provider 到 Riverpod:迁移旧状态管理」这节课中我会学到什么?

将现有的 ChangeNotifier 和 Provider 代码转换为 Riverpod 的编译安全提供者图。 你通过在浏览器中直接运行的动手代码来练习 Flutter Mobile Development,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 Flutter Mobile Development 需要有经验吗?

无需任何先前经验。CoddyKit 上的 Flutter Mobile Development 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 1 节课,共 4 节。

「从 Provider 到 Riverpod:迁移旧状态管理」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 Flutter Mobile Development 课中编写并运行代码吗?

能。每节 Flutter Mobile Development 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 从 Provider 到 Riverpod:迁移旧状态管理
  2. 使用 riverpod_generator 和 @riverpod 生成代码
  3. AsyncNotifier 与 FutureProvider 数据流水线
  4. 提供者作用域、覆盖与 ProviderObserver
← 返回 Flutter Mobile Development