欢迎关注我的微信公众号: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_style
和 migration/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.md
和 v3_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_generator
和build_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.3
→ fvm flutter --version
→ fvm dart pub global run melos bootstrap
→ 在应用目录内运行 fvm flutter run
或 fvm flutter test
如果你发现了更顺畅的迁移技巧或新的演示思路,请告诉我------分享经验总能让下一次升级更容易。