Add architecture documentation and retail system workshop PPT
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
# 03. 状态管理方案
|
||||
|
||||
## 决策
|
||||
|
||||
使用 **[Riverpod](https://riverpod.dev/)**(`flutter_riverpod` + `riverpod_generator` 代码生成),不使用 Bloc/Provider/GetX。
|
||||
|
||||
版本基线:`flutter_riverpod: ^3.4.2`(当前 stable,2026-08 快照,需在实际开工时用 `flutter pub outdated` 复核)。
|
||||
|
||||
## 依赖
|
||||
|
||||
```yaml
|
||||
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](./02-layering.md))。
|
||||
- 每个 `feature_*` 包各自维护自己的 provider,不跨包直接引用另一个 feature 的 provider(同 [01-project-structure.md](./01-project-structure.md) 的 feature 隔离规则);跨 feature 共享的 provider 定义在对应的 `core_*` 包里。
|
||||
|
||||
## 测试
|
||||
|
||||
- `Notifier`/`AsyncNotifier` 的单元测试用 `ProviderContainer` 直接实例化,不依赖 widget tree。
|
||||
- Widget 测试中用 `ProviderScope(overrides: [...])` 注入 mock 依赖。
|
||||
|
||||
## 附录:Riverpod 是什么,日常怎么用
|
||||
|
||||
给还没接触过 Riverpod 的同学看的入门说明。
|
||||
|
||||
### 要解决的问题
|
||||
|
||||
Flutter 官方最早推荐的状态管理方式是 [`InheritedWidget`](https://docs.flutter.dev/data-and-backend/state-mgmt/inherited-widget)——通过 widget 树往下传数据。写法繁琐,社区后来做了一层封装叫 [`Provider`](https://pub.dev/packages/provider),但 `Provider` 本质还是绑定在 widget 树上:拿依赖必须要有 `BuildContext`,写错了会在运行时才报错(比如 `ProviderNotFoundException`),而且没法很方便地在 widget 树之外(比如后台任务、单元测试)读取状态。
|
||||
|
||||
**Riverpod** 是 `Provider` 的原作者 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`:拉取附近门店列表)
|
||||
|
||||
```dart
|
||||
// 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());
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```dart
|
||||
// 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](./02-layering.md) 的跳过 domain 层示例),`StoreListNotifier` 通过 `ref.watch` 拿到接口类型,不关心具体实现——这就是 Riverpod 承担依赖注入职责的地方,不需要额外的 `get_it`。
|
||||
|
||||
### 测试示例
|
||||
|
||||
```dart
|
||||
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,这也是"编译期安全 + 好测试"这条评价的具体体现。
|
||||
|
||||
## 参考链接
|
||||
|
||||
- [Riverpod 官方文档](https://riverpod.dev/)
|
||||
- [riverpod_generator | Dart package](https://pub.dev/packages/riverpod_generator)
|
||||
- [flutter_riverpod | Dart package](https://pub.dev/packages/flutter_riverpod)
|
||||
- [riverpod_lint | Dart package](https://pub.dev/packages/riverpod_lint)
|
||||
- [InheritedWidget(Flutter 官方文档)](https://docs.flutter.dev/data-and-backend/state-mgmt/inherited-widget)
|
||||
- [provider | Dart package](https://pub.dev/packages/provider)
|
||||
Reference in New Issue
Block a user