Files
conti-docs/03-state-management.md
T

5.8 KiB
Raw Blame History

03. 状态管理方案

决策

使用 Riverpodflutter_riverpod + riverpod_generator 代码生成),不使用 Bloc/Provider/GetX。

版本基线:flutter_riverpod: ^3.4.2(当前 stable,2026-08 快照,需在实际开工时用 flutter pub outdated 复核)。

依赖

dependencies:
  flutter_riverpod: ^3.4.2
  riverpod_annotation: ^3.4.2

dev_dependencies:
  riverpod_generator: ^3.4.2
  build_runner: ^2.4.0
  custom_lint: ^0.6.0
  riverpod_lint: ^3.0.0

使用规则

  • 所有跨 widget 共享的状态、依赖注入,统一通过 Riverpod provider 暴露,不额外引入 get_it/provider 等其他 DI 方案。
  • 优先使用 riverpod_generator 的注解写法(@riverpod),不手写裸 Provider/StateNotifierProvider 模板代码。
  • Notifier/AsyncNotifier 用于承载可变的 feature 状态;无状态的计算/依赖注入用普通 Provider
  • domain/data 层的 repository 实现通过 provider 注入到 presentation 层,presentation 只依赖 provider 暴露的接口类型(见 02-layering.md)。
  • 每个 feature_* 包各自维护自己的 provider,不跨包直接引用另一个 feature 的 provider(同 01-project-structure.md 的 feature 隔离规则);跨 feature 共享的 provider 定义在对应的 core_* 包里。

测试

  • Notifier/AsyncNotifier 的单元测试用 ProviderContainer 直接实例化,不依赖 widget tree。
  • Widget 测试中用 ProviderScope(overrides: [...]) 注入 mock 依赖。

附录:Riverpod 是什么,日常怎么用

给还没接触过 Riverpod 的同学看的入门说明。

要解决的问题

Flutter 官方最早推荐的状态管理方式是 InheritedWidget——通过 widget 树往下传数据。写法繁琐,社区后来做了一层封装叫 Provider,但 Provider 本质还是绑定在 widget 树上:拿依赖必须要有 BuildContext,写错了会在运行时才报错(比如 ProviderNotFoundException),而且没法很方便地在 widget 树之外(比如后台任务、单元测试)读取状态。

RiverpodProvider 的原作者 Remi Rousselet 重新设计的下一代方案:把状态容器从 widget 树里剥离出来,变成一套独立的依赖图,BuildContext 不再是拿依赖的必要条件,错误也从运行时提前到编译期发现(比如 provider 类型不匹配会直接编译报错,而不是运行时崩溃)。

核心概念

  1. Provider:声明一个"如何创建某个值"的配方,值可以是同步的、异步的(Future/Stream)、也可以是可变的状态。
  2. Notifier / AsyncNotifier:承载可变状态的载体,通过方法修改状态(类似过去 StateNotifier 的角色,3.x 里统一成 Notifier)。
  3. ref.watch(xxxProvider):在 widget 或另一个 provider 里订阅某个 provider,值变化时自动触发重建/重新计算。
  4. ref.read(xxxProvider):只读取一次当前值,不订阅变化(一般用在按钮点击等一次性事件回调里)。
  5. @riverpod 注解 + 代码生成:手写 Provider/NotifierProvider 样板代码容易出错(尤其是泛型),项目统一用 riverpod_generator 的注解写法,跑 build_runner 自动生成对应的 provider。

使用示例(feature_store:拉取附近门店列表)

// presentation/store_list_notifier.dart
part 'store_list_notifier.g.dart';

@riverpod
class StoreListNotifier extends _$StoreListNotifier {
  @override
  Future<List<Store>> build() async {
    final repository = ref.watch(storeRepositoryProvider);
    return repository.fetchNearbyStores(_currentLat, _currentLng);
  }

  Future<void> refresh() async {
    state = const AsyncLoading();
    state = await AsyncValue.guard(() => build());
  }
}
// presentation/store_list_page.dart
class StoreListPage extends ConsumerWidget {
  const StoreListPage({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final storesAsync = ref.watch(storeListNotifierProvider);

    return storesAsync.when(
      data: (stores) => ListView.builder(
        itemCount: stores.length,
        itemBuilder: (_, i) => ListTile(title: Text(stores[i].name)),
      ),
      loading: () => const CircularProgressIndicator(),
      error: (err, _) => Text('加载失败: $err'),
    );
  }
}

storeRepositoryProvider 定义在 data 层(见 02-layering.md 的跳过 domain 层示例),StoreListNotifier 通过 ref.watch 拿到接口类型,不关心具体实现——这就是 Riverpod 承担依赖注入职责的地方,不需要额外的 get_it

测试示例

test('刷新后状态应更新为最新门店列表', () async {
  final container = ProviderContainer(
    overrides: [
      storeRepositoryProvider.overrideWithValue(FakeStoreRepository()),
    ],
  );
  addTearDown(container.dispose);

  final stores = await container.read(storeListNotifierProvider.future);
  expect(stores, isNotEmpty);
});

ProviderContainer 让整个依赖图脱离 widget 树单独运行,overrides 直接替换掉真实的 repository,这也是"编译期安全 + 好测试"这条评价的具体体现。

参考链接