状态管理
框架层已实现 diff 算法,为什么框架一直在解决局部刷新的问题?
- Diff 发生的时间节点太晚了:当 Flutter 进行
canUpdate比对时,新的 Widget 节点已经被new出来了,且父组件的build()函数已经执行完毕。 - Diff 的目的只是复用 Element:Flutter 仅仅是复用旧的 Element 和 RenderObject 节点,把新的 Widget 配置塞给它们。
小结:原生框架存在 Widget() 和 build()重建,及子节点的属性变化其子树重建时
-
虽然单个 Widget 对象很轻量,但如果一个大页面包含几百上千个 Widget
- 每次根部刷新,所有子 Widget 的构造函数全都会被重新调用一次。
- 内存中会瞬时产生成千上万个废弃的旧 Widget 垃圾对象,直接给 Dart 的垃圾回收(GC)带来巨大压力,引发 GC 停顿,导致界面丢帧、卡顿。
-
CPU 开销:无意义的 Dart 代码逻辑重复执行。每个 Widget 的
build()函数里往往写着不少逻辑(如:数据格式化、字符串拼接、Theme.of(context)哈希检索等)。 -
Layout / Paint 阶段的连锁打脏,当新的 Widget 配置塞给 RenderObject 时:
- 如果某些属性改变(例如文本变长、边距改变),RenderObject 会被标脏
markNeedsLayout()。 - 这会导致 Flutter 重新计算排版(Measure & Layout)。由于布局存在树状依赖,一个子节点的改变可能会引发整棵树重新计算几何尺寸。
- 如果某些属性改变(例如文本变长、边距改变),RenderObject 会被标脏
状态管理解决了那些问题?
-
跨组件传递数据问题
-
局部刷新问题
-
业务逻辑与 UI 视图解耦(架构清晰度)
-
状态的生命周期与资源自动管理(防止内存泄漏)
-
“依赖注入”的编译期检查(Riverpod)
默认状态管理
-
响应式、声明式区别:
- 响应式需要开发者控制刷新状态,例如更新了状态,需要去手动更新view;
- 声明式直接调用
setState(),交给sdk去处理刷新。
-
声明式(flutter、SwiftUI):
-
优势: 让开发者摆脱组件的繁琐控制,聚焦于状态处理
-
缺点:
- 控制刷新范围;
- 逻辑与业务耦合;
- 夸组件访问困难;
-
InheritedWidget:
为什么需要InheritedWidget?
比如中间隔了 9 层,手动怎么实现:
-
传参解决:代码臃肿,中间件被污染且代码难以被维护。
-
全局单例:全局变量无法感知UI 树的局部生命周期,数据变了,UI 也不会自动刷新
Flutter 必须提供一种能够自顶向下隐式共享数据,同时又能精确驱动 UI 依赖更新的机制,这就是 InheritedWidget 诞生的初衷。
原理:
必须要解决的问题:
-
自定义的
InheritedWidget怎么存储?_inheritedElements -
订阅的数据怎么刷新? _dependents()
-
怎么驱动订阅的数据?updateShouldNotify()
-
数据结构:
InheritedWidget在 Element 层采用了 哈希表 + 指针向下传递 的映射结构。普通 Element 节点:在mount阶段直接继承并指向父节点的_inheritedElements指针(零内存开销)-
//1. 每一个 Element 节点内部都维护着一个字段 Map<Type, InheritedElement>? _inheritedElements; //2. Element.mount 源码示意 void mount(Element? parent, Object? newSlot) { _parent = parent; // 直接继承父节点的 Map 引用,内存共享,效率极高 _inheritedElements = parent?._inheritedElements; ... } //例: RootElement (根节点) └── Theme (InheritedWidget A) └── Container (普通节点 1 ) ├── Column (普通节点 2 ) │ └── Text (普通节点 3 ) └── Provider (InheritedWidget B) └── Button (普通节点 4 ) //1. 当遇到 Theme 时,浅拷贝产生 Map 2 ➔ { Type(Theme): ThemeElement } //2. 当遇到 Provider 时,从 Map 2 浅拷贝产生 Map 3 ➔ { Type(Theme): ThemeElement, Type(Provider): ProviderElement }
-
-
查找机制:Theme.of(context) 当子组件通过
context.dependOnInheritedWidgetOfExactType<T>()获取数据时,直接通过_inheritedElements[T]获取对应的InheritedElement- 建立依赖:
InheritedElement内部维护了一个订阅者集合 _dependents:
- 建立依赖:
-
怎么刷新: updateShouldNotify()
- 触发时机:当父级组件重新构建(
rebuild),向树中传入了新的InheritedWidget实例时触发。 - 触发 setState(),调用到widget.update()调到这个方法,触发updateShouldNotify()
- 触发时机:当父级组件重新构建(
-
context.getInheritedWidgetOfExactType()与context.dependOnInheritedWidgetOfExactType<T>()的区别- 是否注册依赖在 _dependents,
dependOnInheritedWidgetOfExactType当前组件会重新 build,getInheritedWidgetOfExactType不会重新 build在点击事件时调用。
- 是否注册依赖在 _dependents,
使用
//定义
class ShareDataWidget extends InheritedWidget {
ShareDataWidget({required this.data, required Widget child}) : super(child: child);
final int data; //需要在子树中共享的数据
//定义一个便捷方法,方便子树中的widget获取共享数据
static ShareDataWidget? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<ShareDataWidget>();
}
//该回调决定当data发生变化时,是否通知子树中依赖data的Widget
//是 widget.update()调到这个方法
@override
bool updateShouldNotify(ShareDataWidget old) {
//如果返回true,则子树中依赖(build函数中有调用)本widget
//的子widget的`state.didChangeDependencies`会被调用
return old.data != data;
}
}
//包裹
ShareDataWidget(
//使用ShareDataWidget
data: count,
child: child,
)
//使用
ShareDataWidget.of(context)!.data
缺点
-
样板代码极多,手写封装繁琐
-
缺少灵活的“生命周期管理与资源释放”
- 原生
InheritedWidget无法自己管理持久状态和监听挂载/销毁 InheritedWidget只是一个纯粹的数据透传容器,它自己不具备异步加载、依赖注入和资源销毁的能力。
- 原生
-
缺乏“条件刷新/局部过滤”机制(Selector / Consumer)
- 局部刷新:整个 model的任一对象改变,都会刷新
- 条件过滤
-
无法优雅地处理“数据组合与链式依赖”(ProxyProvider),原生
InheritedWidget:处理这种嵌套依赖非常痛苦,需要层层嵌套,并在didUpdateWidget中手动传递和更新旧数据。
Provider
原理
它的底层本质是对 Flutter 原生组件 InheritedWidget 进行的高级封装与增强,并结合了 ChangeNotifier (观察者模式) 来解决数据传递与局部刷新的问题。
有InheritedWidget为什么还需要Provider?
-
代码简化
-
提供生命周期管理及资源自动释放
-
提供“条件刷新/局部过滤”
-
ProxyProvider解决层层嵌套问题
如何封装一个Provider:
提供了 child缓存机制,避免了无效的刷新
内部封装了StatefulWidget,实现生命周期管理
-
CommonInheritedProvider extend InheritedWidget:正常Provider的使用,需要自定义 -
CustomModel对象,继承自ChangeNotifier -
ChangeNotifierProvider StatefulWidget-
使用
CommonInheritedProvider包裹data和widget -
充当观察者,监听Model对象属性变化,当对象变化时,进行
setState()刷新UI-
setState()是局部刷新。- 仅使用Provider引用了data的widget刷新,而不会整个外部Widget刷新,并且引用的widget用Builder改变了层级。
- 外部传入的
widget不会刷新,原因是外部的widget缓存;。
-
-
-
ChangeNotifierProvider.of(context) 即Consumer 跨页面获取数据、消费数据,并自动刷
Riverpod
优点:
-
彻底摆脱
BuildContext依赖 -
将错误扼杀在“编译期”(编译期类型安全)
- 泛型强制推导
- 基于指针引用的类型隔离
- 代码生成器
-
支持同类型多实例
缺点:
自由度比较高,新手容易乱写
Bloc / Cubit
Bloc(Business Logic Component)/ Cubit 的出现,就是为了提供强约束的规范化架构。
-
理解Bloc、Cubit
-
Bloc的核心理念是通过事件驱动和响应式编程将 UI 与业务逻辑解耦。
-
核心组件:Event、Bloc、State、Stream(事件和状态传递的载体)
-
// 本质也是InheritedWidget BlocProvider( create: (BuildContext context) => MyHomePage( title: '', ), ); BlocProvider.of<>(context);调用了context.dependOnInheritedWidgetOfExactType() // 本质通过listener调用setState() BlocBuilder( builder: (BuildContext context, state) {}, ); @override Widget build(BuildContext context) { if (widget.bloc == null) { // Trigger a rebuild if the bloc reference has changed. // See https://github.com/felangel/bloc/issues/2127. context.select<B, bool>((bloc) => identical(_bloc, bloc)); } return BlocListener<B, S>( bloc: _bloc, listenWhen: widget.buildWhen, listener: (context, state) => setState(() => _state = state), child: widget.build(context, _state), ); } 通过_stateController.stream绑定了Bloc与BlocBuilder的关系。emit最后是进行标脏 context.read<DemoBloc>().add(DemoEvent()); emit(state); final subscription = value.stream.listen( (dynamic _) => e.markNeedsNotifyDependents(), );
-
-
Bloc局部刷新,使用buildWhen:
-
BlocBuilder<CounterBloc, int>( buildWhen: ( previous, current) => previous != current, builder: (context, count) { return Text( '$count', style: Theme.of(context).textTheme.displayLarge, ); }, )
-
-
GetX
-
GetBuilder:底层原理: GetBuilder 是通过 InheritedWidget 实现的。当 Model 层的数据发生变化时,GetBuilder 会通过 InheritedWidget 机制通知相关的 Widget 进行重建。
-
Obx:底层原理: Obx 使用了 Stream 来实现响应式更新。它订阅了被观察对象(通常是 Rx 类型的变量),当这些对象的值发生变化时,Obx 会收到通知并触发 Widget 的重建。
-
class Controller extends GetxController { var count = 0; void increment() { count++; update(); } } class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { final Controller controller = Get.put(Controller()); // 将 Controller 注入到依赖中 return Scaffold( appBar: AppBar( title: Text('GetX Example'), ), body: Center( child: Obx(() { return Text( 'Counter: ${controller.count}', // 使用 Obx 来自动监听 count 变化 style: TextStyle(fontSize: 30), ); }), ), floatingActionButton: FloatingActionButton( onPressed: controller.increment, // 点击按钮时调用 increment 方法 child: Icon(Icons.add), ), ); } }
-
-
全局生命周期使用:GetxService
-
优缺点: 包含了局部刷新,跨组件问题,生命周期管理,业务逻辑与 UI 解耦等;
- 和 Provider 一样,没有编译检查,Get.find在没有注册时,出现直接报错