SuperAPI AI 工具集

riverpod

编程与开发

查看项目

更新于 10-15 15:25

技能介绍

用途

本技能定义如何在 Flutter 和 Dart 应用中正确使用 Riverpod 进行状态管理,覆盖 Provider 定义、状态生命周期、参数传递、副作用处理与测试等主题。

一、初始化

在 runApp 中直接用 ProviderScope 包裹应用,不要放在 MyApp 内部:

void main() {
  runApp(const ProviderScope(child: MyApp()));
}

安装并使用 riverpod_lint 以启用 IDE 重构并强制最佳实践。

二、定义 Provider

// 函数式 provider(代码生成)
@riverpod
int example(Ref ref) => 0;

// FutureProvider(代码生成)
@riverpod
Future<List<Todo>> todos(Ref ref) async {
  return ref.watch(repositoryProvider).fetchTodos();
}

// Notifier(代码生成)
@riverpod
class TodosNotifier extends _$TodosNotifier {
  @override
  Future<List<Todo>> build() async {
    return ref.watch(repositoryProvider).fetchTodos();
  }

  Future<void> addTodo(Todo todo) async { ... }
}

要求:所有 provider 定义为 final 顶层变量;根据返回类型选择 Provider、FutureProvider 或 StreamProvider;访问 provider 时使用 ConsumerWidget 或 ConsumerStatefulWidget,而非 StatelessWidget/StatefulWidget。

三、使用 Ref

各方法用途:

  • ref.watch:响应式监听,值变化时重建;仅在 build 阶段使用。
  • ref.read:一次性访问,用于回调/Notifier 方法,不要在 build 中使用。
  • ref.listen:命令式订阅,能使用 ref.watch 时优先用 watch。
  • ref.onDispose:provider 状态销毁时清理。
// 在 widget 中
class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final value = ref.watch(myProvider);
    return Text('$value');
  }
}

// 在 provider 中清理
final provider = StreamProvider<int>((ref) {
  final controller = StreamController<int>();
  ref.onDispose(controller.close);
  return controller.stream;
});

禁止在回调、监听器或 Notifier 方法中调用 ref.watch;从 UI 调用 Notifier 方法用 ref.read(yourNotifierProvider.notifier).method();在异步回调中 await 之后使用 ref 前检查 context.mounted。

四、组合 Provider

@riverpod
Future<String> userGreeting(Ref ref) async {
  final user = await ref.watch(userProvider.future);
  return 'Hello, ${user.name}!';
}

使用 ref.watch(asyncProvider.future) 等待异步 provider 解析后的值。Provider 只执行一次并缓存结果,多个 widget 监听同一 provider 共享同一次计算。

五、传参(Families)

@riverpod
Future<Todo> todo(Ref ref, String id) async {
  return ref.watch(repositoryProvider).fetchTodo(id);
}

// 使用
final todo = ref.watch(todoProvider('some-id'));

带参数的 provider 应始终启用 autoDispose 以防内存泄漏;多参数使用 Dart 3 records 或代码生成(天然重写 ==);避免用普通 List 或 Map 作参数(未重写 ==),改用 const 集合、records 或具备正确相等性的类;用 riverpod_lint 的 provider_parameters 规则捕获相等性错误。

六、自动销毁与状态生命周期

使用代码生成时:不再被监听后状态默认销毁,可用 keepAlive: true 退出。不使用代码生成时:状态默认保活,需用 .autoDispose 启用销毁。provider 重算时状态总是被销毁。

// 配合定时器的 keepAlive
ref.onCancel(() {
  final link = ref.keepAlive();
  Timer(const Duration(minutes: 5), link.close);
});

用 ref.onDispose 做清理,但不要在其中有副作用或修改 provider;用 ref.invalidate(provider) 强制销毁,provider 内部用 ref.invalidateSelf();用 ref.refresh(provider) 失效并立即读取新值,始终使用其返回值。

七、预初始化

Provider 默认惰性。要预初始化:

// 在 MyApp 或 ProviderScope 下的专用 widget 中:
Consumer(
  builder: (context, ref, _) {
    ref.watch(myEagerProvider); // 强制初始化
    return const MyApp();
  },
)

将预初始化放在公开 widget(而非 main())中以保证测试行为一致;用 AsyncValue.requireValue 直接读取数据,未就绪时抛出明确错误。

八、执行副作用

@riverpod
class TodosNotifier extends _$TodosNotifier {
  Future<void> addTodo(Todo todo) async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() async {
      await ref.read(repositoryProvider).addTodo(todo);
      return [...?state.value, todo];
    });
  }
}

// UI 中:
ElevatedButton(
  onPressed: () => ref.read(todosNotifierProvider.notifier).addTodo(todo),
  child: const Text('Add'),
)

事件处理器中用 ref.read(不是 ref.watch);副作用后通过直接设置状态、调用 ref.invalidateSelf() 或手动更新缓存来更新状态;UI 中始终处理 loading 与 error 状态;不要在 provider 构造函数或 build 方法中执行副作用。

九、Provider 观察者

class MyObserver extends ProviderObserver {
  @override
  void didUpdateProvider(ProviderObserverContext context, Object? previousValue, Object? newValue) {
    print('[${context.provider}] updated: $previousValue → $newValue');
  }

  @override
  void providerDidFail(ProviderObserverContext context, Object error, StackTrace stackTrace) {
    // 上报到错误服务
  }
}

runApp(ProviderScope(observers: [MyObserver()], child: MyApp()));

十、测试

// 单元测试
final container = ProviderContainer(
  overrides: [repositoryProvider.overrideWith((_) => FakeRepository())],
);
addTearDown(container.dispose);

expect(await container.read(todosProvider.future), isNotEmpty);

// Widget 测试
await tester.pumpWidget(
  ProviderScope(
    overrides: [repositoryProvider.overrideWith((_) => FakeRepository())],
    child: const MyApp(),
  ),
);

每个测试创建新的 ProviderContainer 或 ProviderScope,不在测试间共享状态;对 autoDispose provider 使用 container.listen 而非 container.read 以在测试期间保持状态存活;用 overrides 注入 mock 或 fake;优先 mock 依赖(repository)而非直接 mock Notifier;若必须 mock Notifier,应继承原始类,不要用 implements 或 with Mock;使用代码生成时将 Notifier mock 放在与 Notifier 同一文件中;在 widget 测试中用 ProviderScope.containerOf(tester.element(...)) 获取容器。

参考

  • Riverpod GitHub 仓库

限制

上述规则基于 Riverpod 的代码生成写法,未提供非代码生成方式下各主题的完整对照说明;其他平台支持情况与版本要求暂未提供。