04 状态管理与架构
4.1 状态分类
| 类型 | 例子 | 方案 |
|---|---|---|
| 临时 UI 状态(Ephemeral) | 按钮是否按下、Tab 下标、输入框内容 | StatefulWidget + setState |
| 跨组件/页面共享状态(App State) | 登录用户、购物车、设置、列表数据 | Provider / Riverpod / Bloc |
原则:能用 setState 就不要上全局状态;需要共享时把状态“提升”到公共祖先,或交给状态管理库。
4.2 底层机制:InheritedWidget 与 Listenable
所有状态管理库都建立在这两个基础上,理解它们就理解了 Provider 等库在做什么。
InheritedWidget:沿树向下共享数据
class UserScope extends InheritedWidget {
const UserScope({super.key, required this.user, required super.child});
final User user;
// 子孙通过 context 查找最近的 UserScope,同时注册依赖
static User of(BuildContext context) =>
context.dependOnInheritedWidgetOfExactType<UserScope>()!.user;
// 返回 true 时,所有调用过 of(context) 的子孙会重建
@override
bool updateShouldNotify(UserScope old) => old.user != user;
}
Theme.of(context)、MediaQuery.of(context) 都是这样实现的。查找是 O(1) 的(每个 Element 持有祖先 InheritedElement 的 Map)。
ChangeNotifier / ValueNotifier:可监听对象(≈ LiveData)
final counter = ValueNotifier<int>(0);
ValueListenableBuilder<int>(
valueListenable: counter,
builder: (context, value, child) => Text('$value'), // 只重建这一小块
);
counter.value++;
4.3 主流方案对比
| 方案 | 类比 | 优点 | 缺点 | 适用 |
|---|---|---|---|---|
| Provider | 手工 DI + LiveData | 官方曾推荐,简单 | 依赖 context,运行时找不到会崩 | 小中型项目 |
| Riverpod | Hilt + StateFlow | 编译期安全、不依赖 context、易测试、支持异步/缓存/自动释放 | 概念多 | 中大型项目,推荐 |
| Bloc / Cubit | MVI | 事件→状态,严格单向数据流,可追溯 | 样板代码多 | 大团队、强规范 |
| GetX | — | 上手快 | 侵入性强、隐式魔法多、维护风险 | 不推荐用于长期项目 |
| signals / MobX | — | 细粒度响应式 | 生态较小 | 按需 |
团队选型建议:选 Riverpod 或 Bloc 其中一个并统一。下面分别给出写法。
4.4 Provider(入门)
// 1. 定义 ViewModel
class CartModel extends ChangeNotifier {
final List<Item> _items = [];
List<Item> get items => List.unmodifiable(_items);
int get total => _items.fold(0, (s, e) => s + e.price);
void add(Item item) {
_items.add(item);
notifyListeners(); // ≈ liveData.setValue
}
}
// 2. 在树的上层提供
void main() => runApp(
ChangeNotifierProvider(create: (_) => CartModel(), child: const MyApp()),
);
// 3. 使用
final cart = context.watch<CartModel>(); // 监听,变化时重建当前 Widget
context.read<CartModel>().add(item); // 只读取,不监听(在回调里用)
final total = context.select<CartModel, int>((c) => c.total); // 只监听某个字段
4.5 Riverpod(推荐)
dependencies:
flutter_riverpod: ^3.0.0
riverpod_annotation: ^3.0.0
dev_dependencies:
riverpod_generator: ^3.0.0
build_runner: ^2.4.0
(版本号仅示意,以 flutter pub add 结果为准。)
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() => runApp(const ProviderScope(child: MyApp()));
// 依赖注入:Repository
final feedRepoProvider = Provider<FeedRepository>((ref) => FeedRepository(ref.watch(dioProvider)));
// 异步数据 + 状态(≈ ViewModel + StateFlow<UiState>)
class FeedNotifier extends AsyncNotifier<List<Feed>> {
int _page = 1;
@override
Future<List<Feed>> build() async {
_page = 1;
return ref.read(feedRepoProvider).fetch(page: 1); // 首次加载
}
Future<void> refresh() async {
state = const AsyncLoading();
state = await AsyncValue.guard(build); // 自动捕获异常为 AsyncError
}
Future<void> loadMore() async {
final current = state.value ?? [];
final next = await ref.read(feedRepoProvider).fetch(page: ++_page);
state = AsyncData([...current, ...next]);
}
}
final feedProvider = AsyncNotifierProvider<FeedNotifier, List<Feed>>(FeedNotifier.new);
// UI
class FeedPage extends ConsumerWidget {
const FeedPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final feed = ref.watch(feedProvider);
return feed.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (e, _) => Center(child: Text('加载失败:$e')),
data: (list) => RefreshIndicator(
onRefresh: () => ref.read(feedProvider.notifier).refresh(),
child: ListView.builder(
itemCount: list.length,
itemBuilder: (_, i) => FeedItem.fromModel(list[i]),
),
),
);
}
}
要点:
ref.watch在 build 中使用(订阅);ref.read在回调中使用(一次性读取);ref.listen用来做副作用(弹 Toast、跳转)。autoDispose:没有监听者时自动销毁,等价于 ViewModel 随页面销毁。family:带参数的 provider,例如detailProvider(id)。- 测试时用
ProviderScope(overrides: [feedRepoProvider.overrideWithValue(FakeRepo())])替换依赖。
4.6 Bloc / Cubit
// Cubit:简化版,直接调用方法 emit 新状态
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void inc() => emit(state + 1);
}
// Bloc:事件驱动
sealed class LoginEvent {}
class LoginSubmitted extends LoginEvent { LoginSubmitted(this.phone, this.code); final String phone, code; }
sealed class LoginState {}
class LoginIdle extends LoginState {}
class LoginLoading extends LoginState {}
class LoginSuccess extends LoginState {}
class LoginFailure extends LoginState { LoginFailure(this.msg); final String msg; }
class LoginBloc extends Bloc<LoginEvent, LoginState> {
LoginBloc(this._repo) : super(LoginIdle()) {
on<LoginSubmitted>((e, emit) async {
emit(LoginLoading());
try {
await _repo.login(e.phone, e.code);
emit(LoginSuccess());
} catch (err) {
emit(LoginFailure('$err'));
}
});
}
final AuthRepository _repo;
}
// UI
BlocProvider(create: (_) => LoginBloc(repo), child: const LoginView());
BlocBuilder<LoginBloc, LoginState>(builder: (context, state) => switch (state) {
LoginLoading() => const CircularProgressIndicator(),
_ => ElevatedButton(
onPressed: () => context.read<LoginBloc>().add(LoginSubmitted(phone, code)),
child: const Text('登录')),
});
BlocListener<LoginBloc, LoginState>(listener: (context, state) { /* 跳转、Toast */ }, child: ...);
4.7 推荐分层架构
与 Android 官方推荐架构(UI Layer / Domain Layer / Data Layer)基本一致:
lib/
├── main.dart
├── app/ # App 入口、路由表、主题、全局 Provider
├── core/ # 网络、存储、日志、错误、工具、基础组件
│ ├── network/
│ ├── storage/
│ └── widgets/
├── features/ # 按业务模块拆分(≈ Android 的 feature module)
│ ├── feed/
│ │ ├── data/ # Repository 实现、DTO、API
│ │ ├── domain/ # 实体、用例(小项目可省略)
│ │ └── presentation/ # 页面、组件、Notifier/Bloc
│ └── profile/
└── l10n/ # 国际化
规则:
- UI 只依赖状态对象(Notifier/Bloc),不直接调用 dio。
- Repository 负责组合远端与本地数据,返回领域模型而不是原始 JSON。
- 平台能力(MethodChannel)也封装在 data 层,对上暴露 Dart 接口,便于在测试中替换。
- 大型项目可用 melos 管理多 package 的 monorepo,按 feature 拆 package。
动手练习
- 用 Riverpod 改写第 3 章的 feed 流,实现 loading / error / data 三态与分页。
- 实现一个全局“登录态” provider:未登录时点击“点赞”跳转登录页,登录后自动回到原页面并完成点赞。
- 用
ProviderScope.overrides写一个 Widget 测试,注入假的 Repository。