面试备战 Flutter 08:状态管理,Provider、BLoC 与 Riverpod
面试备战 Flutter 08:状态管理,Provider、BLoC 与 Riverpod
状态管理不是选库题,而是架构题。核心问题是:
- 状态归谁管?
- 生命周期多长?
- 谁能修改?
- 谁会被通知?
- rebuild 范围多大?
- 异步状态如何表达?
1. 状态分类
| 状态 | 例子 | 建议 |
|---|---|---|
| 局部 UI | tab index、按钮选中 | StatefulWidget/ValueNotifier |
| 页面状态 | 列表、loading、error | Provider/Riverpod/BLoC |
| 跨页面状态 | 用户、主题、权限 | 全局 provider/service |
| 复杂流程 | 支付、订单、IM | BLoC/状态机 |
不要所有状态全局化。
2. Provider
Provider 适合依赖注入和中等复杂状态。
底层本质是对 InheritedWidget 的封装:通过 InheritedWidget 沿 Element 树向上查找 + 登记依赖 + 选择性 rebuild。所以面试问“Provider 底层是什么”,答案是 InheritedWidget + 监听(如 ChangeNotifier):
context.watch/Consumer:登记依赖,InheritedWidget 更新后标脏重建。context.read:只取值不登记,所以不能指望它在 build 里触发重建。context.select/Selector:对选出的值做==比较,值不变就不 rebuild,缩小通知粒度。
优点:
- 简单。
- 成熟。
- 接入成本低。
风险:
- ChangeNotifier 变成上帝对象。
- Consumer 范围过大导致 rebuild。
- notifyListeners 粒度粗。
优化:
Selector<UserModel, String>(
selector: (_, model) => model.name,
builder: (_, name, __) => Text(name),
)
3. BLoC
BLoC 是事件进、状态出。
UI -> Event -> BLoC -> State -> UI
适合:
- 复杂状态机。
- 强业务流程。
- 需要单测。
- 多异步事件合并。
代价:
- 模板代码多。
- 简单页面显重。
4. Riverpod
Riverpod 改进 Provider 对 BuildContext 的依赖,组合和测试更强。
优势:
- 用全局
ProviderContainer定位 provider,不再靠 InheritedWidget 在树里查找,因此不会因 Widget 树位置错误抛ProviderNotFoundException,也能在 Widget 树之外读取(Widget 内仍通过ref访问)。 - provider 可组合。
- 生命周期管理更清晰。
- 测试友好。
代价:
- 学习成本高。
- 团队规范要求高。
5. 选择标准
不是越复杂越高级。
🔬 深度扩展:InheritedWidget 与 Provider 的依赖追踪
状态管理面试最容易被追问的是”Provider 底层怎么实现”和”依赖追踪机制”。要能讲清楚 InheritedWidget、Element.dependOnInheritedElement、通知机制、Selector 优化。
扩展1:InheritedWidget 的核心机制
InheritedWidget 源码:
abstract class InheritedWidget extends ProxyWidget {
InheritedElement createElement() => InheritedElement(this);
// 子类决定是否通知依赖者
bool updateShouldNotify(covariant InheritedWidget oldWidget);
}
InheritedElement 的依赖管理:
class InheritedElement extends ProxyElement {
final Map<Element, Object?> _dependents = HashMap<Element, Object?>();
void updated(InheritedWidget oldWidget) {
if ((widget as InheritedWidget).updateShouldNotify(oldWidget)) {
super.updated(oldWidget);
// 通知所有依赖者 rebuild
notifyClients(oldWidget);
}
}
void notifyClients(InheritedWidget oldWidget) {
for (final Element dependent in _dependents.keys) {
notifyDependent(oldWidget, dependent);
}
}
void notifyDependent(covariant InheritedWidget oldWidget, Element dependent) {
dependent.didChangeDependencies(); // 触发 rebuild
}
}
关键点:
- InheritedElement 维护
_dependents列表 - 更新时检查
updateShouldNotify - 如果返回 true,通知所有依赖者
扩展2:context.dependOnInheritedWidgetOfExactType 的完整流程
调用方式:
final theme = context.dependOnInheritedWidgetOfExactType<MyInheritedWidget>();
Element 的实现:
T? dependOnInheritedWidgetOfExactType<T extends InheritedWidget>({Object? aspect}) {
final InheritedElement? ancestor = _inheritedElements == null
? null
: _inheritedElements![T];
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T?;
}
// 向上查找
ancestor = getElementForInheritedWidgetOfExactType<T>();
if (ancestor != null) {
return dependOnInheritedElement(ancestor, aspect: aspect) as T?;
}
return null;
}
InheritedWidget dependOnInheritedElement(InheritedElement ancestor, {Object? aspect}) {
// 建立依赖关系
_dependencies ??= HashSet<InheritedElement>();
_dependencies!.add(ancestor);
ancestor.updateDependencies(this, aspect);
return ancestor.widget as InheritedWidget;
}
完整流程:
1. 调用 dependOnInheritedWidgetOfExactType
2. 向上遍历 Element 树,查找 InheritedElement
3. 找到后调用 dependOnInheritedElement
4. 建立依赖:当前 Element 加入 InheritedElement._dependents
5. 返回 InheritedWidget
6. 后续 InheritedWidget 更新 → 通知依赖者 rebuild
扩展3:Provider 的底层实现
Provider 核心:
class Provider<T> extends InheritedProvider<T> {
Provider({
required Create<T> create,
Widget? child,
}) : super(
create: create,
dispose: null,
child: child,
);
}
class InheritedProvider<T> extends SingleChildStatelessWidget {
Widget buildWithChild(BuildContext context, Widget? child) {
return _InheritedProviderScope<T>(
owner: this,
child: child,
);
}
}
class _InheritedProviderScope<T> extends InheritedWidget {
bool updateShouldNotify(_InheritedProviderScope<T> oldWidget) {
// 检查值是否变化
return oldWidget.value != value;
}
}
关键:Provider 是 InheritedWidget 的封装
扩展4:context.watch vs context.read
watch(建立依赖):
extension WatchContext on BuildContext {
T watch<T>() {
return Provider.of<T>(this, listen: true); // listen: true
}
}
static T of<T>(BuildContext context, {bool listen = true}) {
final inheritedElement = _inheritedElementOf<T>(context);
if (listen) {
// 建立依赖
context.dependOnInheritedElement(inheritedElement);
}
return inheritedElement.value;
}
read(不建立依赖):
extension ReadContext on BuildContext {
T read<T>() {
return Provider.of<T>(this, listen: false); // listen: false
}
}
差异:
| 方法 | 建立依赖 | rebuild | 使用场景 |
|---|---|---|---|
| watch | ✅ | ✅ | build 里读取,需要响应变化 |
| read | ❌ | ❌ | 事件回调里读取,不需要 rebuild |
错误示例:
// ❌ 错误:在 build 里用 read,状态变化不会 rebuild
Widget build(BuildContext context) {
final counter = context.read<Counter>(); // 不会响应变化
return Text('${counter.count}');
}
// ✅ 正确:用 watch
Widget build(BuildContext context) {
final counter = context.watch<Counter>();
return Text('${counter.count}');
}
扩展5:Selector 的优化原理
问题:
// Provider 包含多个字段
class UserModel extends ChangeNotifier {
String name;
int age;
String avatar;
}
// 只需要 name,但整个 widget 都会 rebuild
Widget build(BuildContext context) {
final user = context.watch<UserModel>();
return Text(user.name); // age/avatar 变化也会 rebuild
}
Selector 解决:
Widget build(BuildContext context) {
final name = context.select<UserModel, String>((user) => user.name);
return Text(name); // 只有 name 变化才 rebuild
}
Selector 源码:
class Selector<A, S> extends SingleChildStatefulWidget {
final S Function(A) selector;
final Widget Function(BuildContext, S, Widget?) builder;
final bool Function(S, S)? shouldRebuild;
_SelectorState<A, S> createState() => _SelectorState<A, S>();
}
class _SelectorState<A, S> extends SingleChildState<Selector<A, S>> {
S? value;
Widget buildWithChild(BuildContext context, Widget? child) {
final selected = widget.selector(Provider.of<A>(context));
final shouldRebuild = widget.shouldRebuild ?? (prev, next) => prev != next;
if (value == null || shouldRebuild(value as S, selected)) {
value = selected;
}
return widget.builder(context, value as S, child);
}
}
关键优化:
- selector 提取需要的字段
- shouldRebuild 比较新旧值(默认用 ==)
- 值没变就不 rebuild
扩展6:BLoC 的完整实现
BLoC 核心:
abstract class Bloc<Event, State> {
final _stateController = StreamController<State>.broadcast();
State _state;
Stream<State> get stream => _stateController.stream;
State get state => _state;
void add(Event event) {
// 处理事件,发出新状态
final newState = mapEventToState(event);
_emit(newState);
}
void _emit(State state) {
if (state == _state) return;
_state = state;
_stateController.add(state);
}
Stream<State> mapEventToState(Event event);
}
BlocBuilder:
class BlocBuilder<B extends Bloc<dynamic, S>, S> extends StatefulWidget {
final B bloc;
final Widget Function(BuildContext, S) builder;
_BlocBuilderState<B, S> createState() => _BlocBuilderState<B, S>();
}
class _BlocBuilderState<B extends Bloc<dynamic, S>, S> extends State<BlocBuilder<B, S>> {
late S _state;
StreamSubscription<S>? _subscription;
void initState() {
super.initState();
_state = widget.bloc.state;
_subscribe();
}
void _subscribe() {
_subscription = widget.bloc.stream.listen((state) {
setState(() {
_state = state;
});
});
}
Widget build(BuildContext context) {
return widget.builder(context, _state);
}
}
关键:基于 Stream + setState
扩展7:Riverpod 的 ProviderContainer
Riverpod 不依赖 InheritedWidget:
// 全局 provider
final counterProvider = StateProvider<int>((ref) => 0);
// 读取方式
class MyWidget extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final count = ref.watch(counterProvider);
return Text('$count');
}
}
ProviderContainer 实现:
class ProviderContainer {
final Map<ProviderBase, ProviderElement> _stateByProvider = {};
T read<T>(ProviderBase<T> provider) {
return _readElement(provider).state as T;
}
ProviderElement<T> _readElement<T>(ProviderBase<T> provider) {
return _stateByProvider.putIfAbsent(provider, () {
return provider.createElement()..mount(this);
});
}
}
优势:
- 不依赖 Widget 树位置
- 可在 Widget 外读取
- 更容易测试
补充总结
状态管理的深度记忆点:
- InheritedWidget:维护 _dependents,updateShouldNotify 决定是否通知
- 依赖建立:dependOnInheritedElement 建立双向引用
- watch vs read:watch 建立依赖会 rebuild,read 不建立依赖
- Selector 优化:提取字段 + 值比较,减少不必要 rebuild
- Provider:InheritedWidget 封装,listen 参数控制依赖
- BLoC:Stream + Event → State,BlocBuilder 监听 stream
- Riverpod:ProviderContainer 全局管理,不依赖树位置
面试追问时要能讲出:
- InheritedElement 如何管理依赖(_dependents Map)
- context.watch 的完整流程(查找 → 建立依赖 → 返回值)
- Selector 如何优化(selector 提取 + shouldRebuild 比较)
- BLoC 的核心实现(Stream + StreamController + setState)
- Riverpod vs Provider 的差异(全局 Container vs InheritedWidget)
选择看:
- 业务复杂度。
- 团队熟悉度。
- 测试要求。
- 状态作用域。
- 是否需要事件流。
- rebuild 粒度。
高频追问
Q1:Provider 会导致整页刷新吗?
取决于监听范围。watch/Consumer 放得太高会扩大 rebuild,Selector 可以缩小通知粒度。
Q2:BLoC 的价值是什么?
把用户事件、业务处理、状态输出变成单向流,复杂流程更可测试、更可追踪。
Q3:状态管理最重要的原则?
状态归属清晰,修改入口可控,通知粒度合理,生命周期明确。
6. 异步状态怎么建模?
很多页面不是只有 data,还有:
- loading。
- success。
- empty。
- error。
- refreshing。
- loadingMore。
不要用多个 bool 随意组合:
bool loading;
bool error;
List data;
复杂后会出现不合法状态。
更好的方式是明确状态模型:
sealed class PageState {}
class Loading extends PageState {}
class Success extends PageState {
final List<Item> data;
}
class Failure extends PageState {
final Object error;
}
这样 UI 根据状态渲染,逻辑更清晰。
7. 状态管理和 rebuild 粒度
状态管理库本身不保证性能。
真正影响性能的是:
- 监听范围。
- 状态拆分粒度。
- notify 频率。
- UI 子树大小。
- build 内部是否重计算。
一个设计很差的 Riverpod 也会卡,一个范围控制好的 setState 也可以很高效。
8. 可测试性
BLoC/Riverpod 的优势之一是测试方便。
测试重点:
- 输入事件。
- mock repository。
- 断言状态序列。
- 错误分支。
- 取消和重试。
状态管理不是为了“更高级”,而是让复杂业务状态可预测、可测试。
深挖追问:状态管理选型本质是状态生命周期治理
先把状态分类:
| 类型 | 例子 | 适合位置 |
|---|---|---|
| 局部 UI 状态 | 展开/选中/输入框焦点 | StatefulWidget |
| 页面状态 | 列表数据、筛选条件 | Provider/BLoC/Riverpod |
| 全局会话 | 用户、权限、主题 | App 级 provider |
| 缓存/领域状态 | 商品、房源、消息 | Repository/Domain 层 |
| 异步状态 | loading/data/error/empty | 明确状态机 |
Provider 深挖:
- 底层依赖 InheritedWidget/InheritedElement。
- watch/select 决定 rebuild 粒度。
- ChangeNotifier 简单但容易把多个字段塞成大对象,导致通知过粗。
- dispose 生命周期要和 provider 所在树一致。
BLoC 深挖:
- 价值在于把事件输入、状态输出和副作用隔离。
- 复杂页面可以把异步、并发、重试、取消建模清楚。
- 事件并发策略很关键:restartable、droppable、sequential、concurrent 会产生完全不同的业务语义。
Riverpod 深挖:
- 不依赖 BuildContext,测试和组合更灵活。
- provider graph 可以表达依赖。
- autoDispose 能降低泄漏,但要处理页面返回后的缓存策略。
- invalidate/refresh 要明确,否则容易造成重复请求。
异步状态不要用几个 bool 乱拼:
sealed class PageState {}
class Loading extends PageState {}
class Data extends PageState { ... }
class Empty extends PageState {}
class Error extends PageState { ... }
面试回答:
我不会为所有页面套同一种状态管理。简单局部状态用 StatefulWidget;跨组件共享用 Provider/Riverpod;复杂事件流和可测试状态机用 BLoC。关键是状态所有权、生命周期、订阅粒度和副作用边界。
一句话总结
状态管理的本质是状态边界和数据流设计,Provider、BLoC、Riverpod 只是不同复杂度下的工程工具。