Riverpod 3.0 关键变化与实战用法

欢迎关注我的微信公众号:OpenFlutter,感谢。 问:我们为什么要现在重新审视 Riverpod 3.0?

Riverpod 3.0 不仅仅是版本号的提升------它重新思考了我们处理状态和异步流的方式。当发布通知出来时,我正在维护一个 Riverpod 2.x 的代码库,我一直在想:"它看起来很熟悉,但到底改变了什么?"这个问题促使我用 Flutter 3.35.3 + FVM 构建了一个可重现的示例仓库,并仔细研究了那些在投入生产环境之前我想验证的改动。

问:我需要什么才能开始试验?

  • 演示仓库:github.com/curogom/riverpod_3
  • 应用示例:apps/
  • 共享仓库:packages/shared_repos/
  • 迁移演练:migration/

重点关注:

  • 每个示例都用 FVM 固定了版本,因此你可以随时重现相同的环境。
  • 多包布局让你可以在应用、共享仓库和迁移代码之间进行并排探索。
  • 其中包含测试套件,因此你可以验证行为,而不是仅仅相信描述。

问:Riverpod 3.0 中最值得注意的变化是什么?

1. FutureProvider 现在支持指数级回退重试

FutureProvider 可以自动重试失败的调用,而不是强迫你手动编写 try-catch 代码块。你只需要切换重试策略即可。

实际应用中的亮点:

  • 在网络不稳定的情况下,用户看到的是 UI "闪烁并恢复",而不是硬性失败。
  • 对于必须快速失败的场景,你可以为每个 Provider 单独禁用重试。
  • 追踪重试尝试次数变得轻而易举,使 QA 和调试更加顺畅。

尝试一下: apps/network_retry_demo

  • 直接在 UI 中切换全局重试或特定 Provider 的异常。
  • RetryLogger 将尝试次数暴露为一个流,供 UI 和测试进行消费。
dart 复制代码
// apps/network_retry_demo/lib/main.dart  
final userFutureProvider = FutureProvider.autoDispose<String>((ref) async {  
final enableRetry = ref.watch(retryToggleProvider);  
final disableForThis = ref.watch(disableForUserProvider);  
final logger = ref.watch(retryLoggerProvider);  
final service = _FlakyUserService(logger: logger);
dart 复制代码
  if (!enableRetry || disableForThis) {
    return service.fetchUser();
  }  const maxAttempts = 3;
  for (var attempt = 0; attempt < maxAttempts; attempt++) {
    try {
      logger.recordAttempt();
      return await service.fetchUser();
    } catch (err) {
      if (attempt == maxAttempts - 1) rethrow;
      await Future.delayed(Duration(milliseconds: 200 * pow(2, attempt).toInt()));
    }
  }
  throw StateError('Unreachable');
});

Unit test: apps/network_retry_demo/test/retry_test.dart

dart 复制代码
test('retry succeeds after transient failure', () async {  
final logger = RetryLogger();  
final container = ProviderContainer(overrides: [  
retryLoggerProvider.overrideWithValue(logger),  
]);  
addTearDown(() {  
logger.dispose();  
container.dispose();  
});
final result = await container.read(userFutureProvider.future);  
expect(result, equals('Riverpod Fan'));  
});

2. Notifier 重建意味着"提取你的资源"

从 3.0 版本开始,Notifier / AsyncNotifier 每次重建时都会创建新的实例。2.x 版本中那种伪单例的行为不再适用,因此将计时器(Timers)或控制器(Controllers)保留在 Notifier 内部会导致资源泄露(Memory Leaks)。

修复方法是:将这些资源拆分到独立的 Provider 中,并使用 ref.onDispose 来绑定它们的生命周期。

实操仓库: apps/counter_app

dart 复制代码
// apps/counter_app/lib/counter.dart  
final stopwatchRepoProvider = Provider<StopwatchRepo>((ref) {  
    final repo = StopwatchRepo();  
    ref.onDispose(repo.dispose);  
    return repo;  
});
dart 复制代码
class CounterNotifier extends Notifier<int> {
  late final StopwatchRepo _repo = ref.read(stopwatchRepoProvider);
  @override
  int build() {
    _repo.start();
    ref.onDispose(_repo.stop);
    return 0;
  }
  void increment() => state++;
  Future<void> persist() async {
    final repo = ref.read(persistenceRepoProvider);
    await repo.write('counter', state);
    if (!ref.mounted) return;
  }
}

你可以比较 migration/v2_stylemigration/v3_final 中重构前后的代码。 "资源在 Notifier 内部""资源在外部提供 + 销毁" 之间的差异变得显而易见。

3. ref.mounted 作为异步流的安全网

长时间的异步任务最终会在 Notifier 销毁后完成;ref.mounted 让你能用一行代码就实现一个安全守卫。

dart 复制代码
Future<void> persist() async {
  final repo = ref.read(persistenceRepoProvider);
  await repo write('counter', state);
  if (!ref.mounted) return; // prevent updates after dispose
}

我(现在)将这种模式视为强制性的,并通过使 Provider 失效并验证 ref.mounted 是否切换为 false 来在测试中断言它。

4. StreamProvider 在无人监听时暂停

StreamProvider 终于在没有监听者时会暂停订阅了,这能防止隐性的资源泄漏。

  • 演示: apps/stream_pause_demo
  • 切换开关,计时器流会立即暂停,同时 UI 显示"Stream paused"。
  • TickerRepo 作为 Provider 被注入,这样你就可以在其他地方重用它。
dart 复制代码
// apps/stream_pause_demo/lib/main.dart
final tickerRepoProvider = Provider<TickerRepo>((ref) => TickerRepo());
final listenToggleProvider = NotifierProvider<ListenToggleNotifier, bool>(ListenToggleNotifier.new);
final tickStreamProvider = StreamProvider<int>((ref) {
  final ticker = ref.watch(tickerRepoProvider);
  return ticker.tick();
});

问:我该如何演练这些迁移示例?

migration/ 目录包含了两个独立的软件包,因此你无需在分支之间来回切换,就能同时探索遗留(2.x)和更新(3.0)的风格。

  • migration/v2_style 捕获了以 StateNotifierProvider 为中心的 2.x 模式。查看 lib/counter.dart 可以回顾资源是如何存放在 Notifier 内部的。
  • migration/v3_final 应用了 3.0 的重构:资源被提取到独立的 Provider 中,生命周期通过 ref.onDispose 进行管理,并且切换使用了 NotifierProvider

每个文件夹都有一个指南(v2_style/README.mdv3_final/README.md)来描述更改内容以及如何运行代码。在运行 fvm use 3.35.3 之后,运行 fvm flutter test 来确认两个软件包是否仍然通过测试。

差异对比示例:

dart 复制代码
// v2_style/lib/counter.dart (StateNotifier baseline)
final counterProvider = StateNotifierProvider<CounterNotifier, int>((ref) {
  return CounterNotifier();
});
dart 复制代码
class CounterNotifier extends StateNotifier<int> {
  CounterNotifier() : super(0);
  void increment() => state++;
}
// v3_final/lib/counter.dart (Notifier + external resource)
final counterProvider = NotifierProvider<CounterNotifier, int>(CounterNotifier.new);
class CounterNotifier extends Notifier<int> {
  late final StopwatchRepo _repo;
  @override
  int build() {
    _repo = ref.read(stopwatchRepoProvider);
    _repo.start();
    ref.onDispose(_repo.stop);
    return 0;
  }
  void increment() => state++;
}

遵循这个流程,就能清楚地知道如何通过将资源卸载到专用的 Provider 中,来防止 Notifier 重建(regeneration)带来的问题。

问:你是如何整理开发环境的?

没有工具的情况下进行迁移是很鲁莽的,所以我将 Linting 检查、代码生成和多包工作流整合到了同一个操作中。

  • melos.yaml + FVM (fvm use 3.35.3) 保持了应用和包的版本一致。
  • analysis_options.yaml 启用了 riverpod_lint,以便在编译时捕获对 watch/read 的错误使用。
  • apps/counter_app 中已包含 riverpod_generatorbuild_runner,因此运行 melos run build 就能按需重新生成代码。

问:我们如何进行持久化和数据变更(Mutations)的实验?

Riverpod 3.0 发布了实验性的持久化/数据变更 API。它们还不是最终版本,但你可以通过存储状态和广播更新来模拟这种体验。

dart 复制代码
// packages/shared_repos/lib/src/persistence_repo.dart
class MockPersistenceRepo {
  final Map<String, Object?> _store = <String, Object?>{};
  final _streamController = StreamController<Map<String, Object?>>.broadcast();
dart 复制代码
Future<void> write(String key, Object? value) async {
    _store[key] = value;
    _streamController.add(Map<String, Object?>.unmodifiable(_store));
  }  Stream<Map<String, Object?>> get changes => _streamController.stream;
}

按下"持久化快照"(Persist snapshot)按钮后,演示应用会将当前状态写入并(通过 StreamBuilder)显示这些变化。当你的持久化层准备就绪时,只需替换掉模拟代码即可。

问:哪些迁移清单效果最好?

  • 将内部资源(秒表、计时器、控制器等)转移到独立的 Provider 中,并使用 ref.onDispose 进行销毁。
  • 在全局和每个 Provider 级别定义重试策略,并辅以测试来保障。
  • 确保每个异步方法都以 if (!ref.mounted) return; 结束。
  • 明确了解 StreamProvider 的订阅和暂停行为。
  • 采用 Linting、代码生成和多包工具来尽早发现问题。

你可以直接遵循 migration/ 目录中的流程------运行代码比比较分支要快得多。

问:最后的想法?

Riverpod 3.0 的意义远不止于语法上的优化;它重塑了异步和状态管理的故事。拥抱自动重试、Notifier 重建、ref.mounted 以及 StreamProvider 的暂停特性,你就能消除大量潜在的遗留风险。运行这些演示,阅读测试代码,然后决定哪些更新最适合你的代码库。

建议的命令流程: fvm use 3.35.3fvm flutter --versionfvm dart pub global run melos bootstrap → 在应用目录内运行 fvm flutter runfvm flutter test

如果你发现了更顺畅的迁移技巧或新的演示思路,请告诉我------分享经验总能让下一次升级更容易。

相关推荐
二十雨辰3 小时前
vite与ts的结合
开发语言·前端·vue.js
我是日安3 小时前
从零到一打造 Vue3 响应式系统 Day 25 - Watch:清理 SideEffect
前端·javascript·vue.js
岁月宁静3 小时前
AI 时代,每个程序员都该拥有个人提示词库:从效率工具到战略资产的蜕变
前端·人工智能·ai编程
小高0073 小时前
🤔「`interface` 和 `type` 到底用哪个?」——几乎每个 TS 新手被这个选择灵魂拷问。
前端·javascript·typescript
行走在顶尖3 小时前
代码管理
前端
_AaronWong3 小时前
Electron IPC 自动化注册方案:模块化与热重载的完美结合
前端·electron·node.js
我的div丢了肿么办3 小时前
vue3使用h函数如何封装组件和$attrs和props的区别
前端·javascript·vue.js
答案answer3 小时前
你不知道的Three.js性能优化和使用小技巧
前端·性能优化·three.js
自由的疯3 小时前
java调chrome浏览器显示网页
java·前端·后端