面试备战 Flutter 22:状态管理,Provider、Bloc 与 Riverpod
面试备战 Flutter 22:状态管理,Provider、Bloc 与 Riverpod
Flutter 状态管理面试不能只回答“我用 Riverpod”。真正要讲清楚的是:
- 这个项目的状态复杂度是什么?
- 为什么 Provider 不够?
- 为什么 Bloc 太重?
- Riverpod 解决了哪些工程问题?
autoDispose和family解决的不是语法问题,而是什么生命周期和缓存问题?
一句话结论:
Provider 适合简单依赖注入和 ChangeNotifier 状态暴露;Bloc 适合事件明确、流程复杂、团队需要强约束的业务;Riverpod 更适合中大型 Flutter 项目里可组合、可测试、可参数化、可自动释放的状态依赖图。
1. 先区分状态类型
状态管理选型前,先区分状态。
| 状态类型 | 示例 | 适合放哪里 |
|---|---|---|
| 局部 UI 状态 | tab index、checkbox、输入框展开 | StatefulWidget / Hook |
| 页面状态 | loading、error、data、分页 | Provider / Notifier |
| 应用状态 | 登录态、用户信息、主题 | 全局 provider / repository |
| 缓存状态 | userId 对应用户、query 对应搜索结果 | family provider / repository cache |
| 流程状态 | 下单、支付、上传、多步骤表单 | Bloc / StateMachine / Notifier |
不要把所有状态都塞进一个全局 Store。状态越全局,生命周期越长,越容易造成刷新范围大、缓存不可控、测试困难。
2. Provider 是什么?
Provider 官方定位是 InheritedWidget 的易用封装。它解决的是:
- 依赖注入。
- 跨组件读取对象。
- 自动创建/释放对象。
context.watch/read/select。Consumer/Selector控制局部 rebuild。
Provider 本身没问题,它非常适合简单项目。
典型写法:
class CounterModel extends ChangeNotifier {
int count = 0;
void increment() {
count++;
notifyListeners();
}
}
ChangeNotifierProvider(
create: (_) => CounterModel(),
child: const CounterPage(),
);
消费:
final count = context.watch<CounterModel>().count;
或者降低刷新粒度:
final count = context.select<CounterModel, int>((model) => model.count);
3. 为什么不用 Provider?
不是因为 Provider 差,而是它的默认组合在复杂项目里容易暴露边界。
3.1 ChangeNotifier 通知粒度粗
notifyListeners() 不携带“哪个字段变了”的语义。所有监听这个 notifier 的地方都可能被通知。
虽然可以用 Selector / context.select 优化,但这依赖开发者持续自律。
3.2 状态容易变成大对象
项目推进后,一个 UserModel 可能逐渐包含:
- 用户信息。
- 登录态。
- 权限。
- 页面 loading。
- 错误消息。
- 请求方法。
- 缓存。
- 跳转副作用。
最后它既是状态,又是 service,又是 controller。
3.3 依赖 BuildContext
Provider 的读取通常绑定 BuildContext。这对 UI 层很自然,但业务层、测试层、纯 Dart 层会变得不够干净。
3.4 参数化状态不自然
比如:
userProvider(userId)
searchProvider(keyword, page)
messageProvider(conversationId)
Provider 可以做,但没有像 Riverpod family 那样自然的参数化实例和缓存模型。
3.5 异步状态要手写
loading、error、data、refresh、retry、cancel 都要自己建模。项目里容易出现多个页面各写一套。
4. Bloc 是什么?
Bloc 强调:
Event -> Bloc -> State
或者 Cubit 简化为:
method call -> Cubit -> State
Bloc 的优势是规范清晰:
- 输入是事件。
- 输出是状态。
- UI 只订阅状态。
- 业务逻辑从 Widget 中剥离。
- 测试友好。
- 适合复杂流程。
典型场景:
- 登录流程。
- 支付流程。
- 表单提交。
- 上传队列。
- 多步骤审批。
- WebSocket 状态机。
5. 为什么不用 Bloc?
Bloc 的问题不是能力不足,而是工程重量。
常见代价:
- Event、State、Bloc、Builder、Listener 模板代码多。
- 简单页面显得过度设计。
- 很多局部状态被迫抽象成事件。
- 参数化缓存仍要额外设计。
- 团队不严格建模时,Bloc 也会变成大 controller。
不用 Bloc 的话术不要说“Bloc 不好”,而应该说:
Bloc 很适合事件流清晰、流程复杂、需要强约束和可测试性的业务。但我们项目更多是页面数据请求、参数化缓存、局部异步状态组合,用 Riverpod 能以更低模板成本表达依赖图和生命周期。
6. Riverpod 解决了什么?
Riverpod 的重点不是“Provider 新版本”,而是把状态从 Widget tree 中抽离成独立依赖图。
核心能力:
- 不依赖
BuildContext。 - provider 可以 watch provider。
- 统一同步/异步状态。
- 支持 family 参数化。
- 支持 autoDispose 生命周期。
- 支持 override 测试。
- 支持 select 降低 rebuild。
- 支持代码生成减少样板。
典型写法:
final userRepositoryProvider = Provider<UserRepository>((ref) {
return UserRepository(ref.watch(dioProvider));
});
final userProvider = FutureProvider.autoDispose.family<User, String>((ref, id) async {
final repository = ref.watch(userRepositoryProvider);
return repository.fetchUser(id);
});
消费:
final user = ref.watch(userProvider(userId));
return switch (user) {
AsyncData(:final value) => Text(value.name),
AsyncError(:final error) => Text('$error'),
_ => const CircularProgressIndicator(),
};
7. Riverpod 的优势逐条讲
7.1 不依赖 BuildContext
业务依赖可以在 provider 之间组合,不需要到处传 context。
这让 repository、use case、测试更干净。
7.2 AsyncValue 统一异步状态
不用每个页面手写:
bool loading;
Object? error;
T? data;
AsyncValue<T> 直接表达:
loading | data | error
7.3 依赖图可组合
provider 可以依赖其他 provider:
final profileProvider = FutureProvider.autoDispose((ref) async {
final token = ref.watch(tokenProvider);
final api = ref.watch(apiProvider);
return api.fetchProfile(token);
});
当 token 变化时,profile 会自动失效并重算。
7.4 测试友好
可以 override:
final container = ProviderContainer(
overrides: [
userRepositoryProvider.overrideWithValue(FakeUserRepository()),
],
);
不用启动完整 Widget tree。
8. AutoDispose 为什么需要?
Riverpod 官方文档说明:autoDispose 可以在 provider 不再被使用时自动销毁关联资源;对于带参数的 provider,官方也建议启用自动释放,否则每个参数组合都会创建一份状态,可能导致内存泄漏。
它解决的是生命周期问题,不只是省内存。
适合:
- 页面离开后取消网络请求。
- 搜索参数变化后释放旧请求。
- WebSocket / StreamController 关闭。
- Timer 释放。
- family 参数缓存清理。
示例:
final searchProvider = FutureProvider.autoDispose.family<SearchResult, String>((ref, keyword) async {
final cancelToken = CancelToken();
ref.onDispose(() => cancelToken.cancel('disposed'));
final response = await dio.get(
'/search',
queryParameters: {'q': keyword},
cancelToken: cancelToken,
);
return SearchResult.fromJson(response.data as Map<String, dynamic>);
});
9. autoDispose 什么时候触发?
可以这样理解:
最后一个 listener 移除
-> onCancel
-> 等一帧
-> 如果仍无人监听
-> dispose
-> onDispose 回调
这个“等一帧”的设计可以避免短时间移除又重新监听导致的抖动。
还可以用 ref.keepAlive() 做更细的缓存策略:
final articleProvider = FutureProvider.autoDispose.family<Article, String>((ref, id) async {
final article = await repository.fetchArticle(id);
ref.keepAlive();
return article;
});
更实际的策略是:成功结果保留一段时间,失败结果不保留。
10. Family 怎么实现?
Riverpod 官方文档把 family 类比成 Map。这个类比非常准确:
普通 provider: userProvider -> one state
family provider: userProvider(id) -> one state per id
例如:
userProvider("1") -> User state A
userProvider("2") -> User state B
userProvider("3") -> User state C
每个参数组合都有独立状态、独立缓存、独立生命周期。
11. family 参数有什么要求?
参数必须稳定,尤其要有一致的 == 和 hashCode。
错误倾向:
ref.watch(searchProvider(SearchArgs(keyword: keyword, page: page)));
如果 SearchArgs 没有值相等,每次 build 都是新对象,缓存会失效。
推荐用 immutable value object:
class SearchArgs with _$SearchArgs {
const factory SearchArgs({
required String keyword,
required int page,
}) = _SearchArgs;
}
12. Provider、Bloc、Riverpod 怎么选?
| 场景 | 推荐 | 原因 |
|---|---|---|
| 很小页面、简单共享对象 | Provider | 成本低,团队容易理解 |
| 表单/支付/上传等复杂流程 | Bloc | 事件和状态清晰,可测试性强 |
| 页面异步数据、参数化缓存 | Riverpod | family + autoDispose 天然匹配 |
| 大量 repository/service 注入 | Riverpod / Provider | Riverpod 更脱离 context |
| 强团队规范、业务状态机 | Bloc | 约束更硬 |
| 中大型 App 综合状态 | Riverpod | 组合、测试、生命周期更均衡 |
13. 面试怎么回答?
可以这样答:
Provider 是 InheritedWidget 的易用封装,适合简单依赖注入和 ChangeNotifier 状态暴露。但在中大型项目里,ChangeNotifier 的通知粒度偏粗,状态容易堆成大对象,异步状态和参数化缓存需要自己维护。
Bloc 的优势是事件输入、状态输出,规范强、可测试,适合复杂流程。但简单页面模板成本高,如果业务主要是参数化请求、异步缓存和局部组合状态,Bloc 会显得偏重。
Riverpod 的优势是状态脱离 BuildContext,provider 可以组成依赖图,AsyncValue 统一异步状态,family 表达参数化缓存,autoDispose 管理页面级生命周期和资源释放。对我们这种页面数据和混合业务较多的项目,它在工程复杂度和可维护性之间更平衡。
参考资料
- Provider package: https://pub.dev/packages/provider
- Bloc documentation: https://bloclibrary.dev/
- Riverpod providers: https://riverpod.dev/docs/concepts2/providers
- Riverpod autoDispose: https://riverpod.dev/docs/concepts2/auto_dispose
- Riverpod family: https://riverpod.dev/docs/concepts2/family
- Flutter state management options: https://docs.flutter.dev/data-and-backend/state-mgmt/options