前言
好久没有写源码分析相关的文章了,之前快速上手了get状态库,现在回头看了源码,以前没想通的都开窍了,使用不当的地方也做了调整。
通过本篇文章,你将了解到:
- 什么是状态管理?
- 设计模式之观察者模式
- get 如何实现状态管理?
- Obx和obs如何配合?
- get 使用建议
1. 什么是状态管理?
比如我们有个UI需要显示当前分数第一名的同学:
void main() {
runApp(MaterialApp(
home: NumberOne(),
));
}
class NumberOne extends StatelessWidget {
final String name = "fish";
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('摸鱼排行榜'),
),
body: Center(
child: Text(
'第一名: $name',
style: TextStyle(fontSize: 24),
),
),
);
}
}
此时的UI是写死的,第一名永远是"fish",UI状态是固定的,不需要管理。
然而,排行榜是动态变化的,显示的第一名需要变化,改造如下:
class NumberOne extends StatefulWidget {
@override
_NumberOneState createState() => _NumberOneState();
}
class _NumberOneState extends State<NumberOne> {
String name = "fish";
void _refreshLeaderboard() {
setState(() {
// 这里可以添加刷新排行榜的逻辑
name = "fish${Random().nextInt(100)}"; // 示例:更新名字
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('摸鱼排行榜'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'第一名: $name',
style: TextStyle(fontSize: 24),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _refreshLeaderboard,
child: Text('刷新排行榜'),
),
],
),
),
);
}
}
当点击按钮"刷新排行榜"时,第一名的名字随机变化,同时在UI上展示最新的名字。
此处涉及到了两个知识点:StatefulWidget和State。
StatefulWidget 表示该Widget是有状态,它的状态通过State来体现。
"有状态"的意思是UI的状态是可变的,当刷新排行榜时,实际是发生了两件事:
- 修改了变量name的值
- 通过调用setState,触发了组件的build方法进行重绘
setState里的逻辑是:先回调了执行闭包代码(修改name),再触发组件重绘。
将_refreshLeaderboard方法改造如下更好理解:
void _refreshLeaderboard() {
// 这里可以添加刷新排行榜的逻辑
name = "fish${Random().nextInt(100)}"; // 示例:更新名字
setState(() {
});
}
数据驱动UI状态变化,管理UI状态变化的过程称为状态管理,setState是Flutter里最基础的状态管理方式
2. 设计模式之观察者模式
虽然setState能够实现状态管理,试想一下:如果UI状态比较复杂,依赖很多数据,每次每个数据变化都需要显式写一下setState闭包逻辑,想想都比较麻烦,那是否可以将setState封装起来,当数据变化的时候自动调用setState方法,如此一来只需要关注数据变化即可。
这场景符合设计模式里的观察者模式。
实现观察者模式有两个核心点:
- 观察者(observer)向被观察者(subject)注册监听(回调)
- 被观察者状态变化后通过回调通知观察者
由此可知,UI属于观察者,数据属于被观察者,当UI向数据注册观察者,数据变化之后通知UI,而后UI调用setState方法,一次自动状态管理就完成了。
3. get 如何实现状态管理?
为了更好地处理状态管理,许多优秀的三方框架大显神通,本次我们主要分析其中的佼佼者:get。
将上面的NumberOne改造如下:
class NumberOne extends StatelessWidget {
String name = "fish";
final controller = NumberOneController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('摸鱼排行榜'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
GetBuilder<NumberOneController>(
builder: (_) => Text(
'第一名: ${controller.name}',
style: TextStyle(fontSize: 24),
),
init: controller),
ElevatedButton(
child: Text('刷新排行榜'),
onPressed: controller.updateName,
),
],
),
),
);
}
}
class NumberOneController extends GetxController {
var name = "fish";
void updateName() {
name = "fish${Random().nextInt(100)}";
update();
}
}
当点击"刷新排行榜"时,UI自动更新了,此时并没有看到任何地方调用了setState。
当需要观察UI变化时,只需要使用GetBuilder包裹对应的组件,然后数据都定义在Controller里,当数据发生变化时更新controller即可自动刷新UI。
接下来深入源码探索其实现。
GetBuilder 继承自StatefulWidget,传入的builder就是用来构造目标组件。
当State调用到initState时:
_subscribeToController-->controller?.addListener(getUpdate)-->_updaters!.add(listener)
_updaters是数组,存储着listener(getUpdate)。
getUpdate实现如下:
mixin GetStateUpdaterMixin<T extends StatefulWidget> on State<T> {
// To avoid the creation of an anonym function to be GC later.
// ignore: prefer_function_declarations_over_variables
/// Experimental method to replace setState((){});
/// Used with GetStateUpdate.
void getUpdate() {
if (mounted) setState(() {});
}
}
看到了熟悉的setState。
也就是说GetBuilder作为观察者,Controller作为被观察者,GetBuilder向Controller注册了监听getUpdate,当Controller发生变化时会调用getUpdate,而该方法里有setState,如此便达到了刷新的目的。
当Controller调用:
updateName-->update-->refresh-->_notifyUpdate
实现如下:
void _notifyUpdate() {
for (var element in _updaters!) {
element!();
}
}
_updaters是数组,存储着上面的getUpdate,element!()即是执行getUpdate。
GetBuilder X Controller 即是典型的观察者模式的实现。
4. Obx和obs如何配合?
GetBuilder X Controller 配合下,虽然已经不用显式调用setState,但还是需要额外调用Controller.update方法,还是有优化的空间。
此时轮到Obx和obs出场,继续改造上面的NumberOne。
class NumberOne extends StatelessWidget {
String name = "fish";
final numberOneEntity = NumberOneEntity();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('摸鱼排行榜'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Obx(() => Text(
'第一名: ${numberOneEntity.name}',
style: TextStyle(fontSize: 24),
)),
ElevatedButton(
child: Text('刷新排行榜'),
onPressed: () {
numberOneEntity.name.value = "fish${Random().nextInt(100)}";
},
),
],
),
),
);
}
}
class NumberOneEntity {
var name = "fish".obs;
}
此时无需继承自Controller,也无需显式调用Controller.update方法,仅仅只需要聚焦于数据的改变。
虽然Obx+obs写起来比GetBuilder+Controller简单很多,但前者原理比后者复杂不少。
依然是观察者模式,只是有两层的观察者模式。
- Obx是观察者,obs是被观察者,当obs发生变化时通知Obx刷新UI
- Obx、obs内部分别有观察者和被观察者
接着分析其源码,先看Obx。
Obx 继承自StatefulWidget,核心是ObxState,重点关注里面的一个属性和三个方法:
final _observer = RxNotifier();
void initState() {
super.initState();
subs = _observer.listen(_updateTree, cancelOnError: false);
}
void _updateTree(_) {
if (mounted) {
setState(() {});
}
}
@override
Widget build(BuildContext context) =>
RxInterface.notifyChildren(_observer, widget.build);
_observer.listen:Obx内部的观察者向被观察者添加监听。
_updateTree 很眼熟,它的实现就是调用了setState方法,根据之前的经验,我们需要按图索骥看谁最终调用了_updateTree即可,也就是说找到被观察者是谁。
StreamSubscription<T> listen(
void Function(T) onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) =>
subject.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError ?? false,
);
LightSubscription<T> listen(void Function(T event) onData,
{Function? onError, void Function()? onDone, bool? cancelOnError}) {
final subs = LightSubscription<T>(
removeSubscription,
onPause: onPause,
onResume: onResume,
onCancel: onCancel,
)
..onData(onData)
..onError(onError)
..onDone(onDone)
..cancelOnError = cancelOnError;
addSubscription(subs);
onListen?.call();
return subs;
}
此处的subject(GetStream对象)就是被观察者,onData对应的是传入的_updateTree。
listen方法里构造LightSubscription对象,onData存储到了LightSubscription对象里,而后将LightSubscription对象存储在GetStream维护的数组:_onData里:
至此,理出来的逻辑为:
Obx的State里持有RxNotifier(_observer)-->GetStream(subject)-->List<LightSubscription>(_onData)-->OnData(_data)
_data==_updateTree
括号里是对应的变量名
Obx里的观察者模式(ObxState是观察者,LightSubscription是被观察者)已经搭建起来了,就看是什么时候被触发了。
当Obx State.build执行时会调用RxInterface.notifyChildren(_observer, widget.build)。
static T notifyChildren<T>(RxNotifier observer, ValueGetter<T> builder) {
final oldObserver = RxInterface.proxy;
RxInterface.proxy = observer;
final result = builder();
if (!observer.canUpdate) {
RxInterface.proxy = oldObserver;
throw """
}
RxInterface.proxy = oldObserver;
return result;
}
核心两点:
- RxInterface.proxy = observer,将Obx里的_observer存储到静态变量里
- 执行Obx的闭包,也就是Obx包裹的组件
- 将静态变量指向老的对象(为null),用以解除全局对象持有UI,避免内存泄漏
以上Obx的主要内容结束了。
接着看obs相关内容。
class NumberOneEntity {
var name = "fish".obs;
}
"fish".obs 是借助了语言上的扩展功能:
extension StringExtension on String {
/// Returns a `RxString` with [this] `String` as initial value.
RxString get obs => RxString(this);
}
基本类型:String、int、bool、double,引用类型都有对应的扩展,核心代码是在父类_RxImpl里。
abstract class _RxImpl<T> extends RxNotifier<T> with RxObjectMixin<T>
继承自RxNotifier,Obx里也持有了RxNotifier的示例。
Obx(() => Text(
'第一名: ${numberOneEntity.name}',
style: TextStyle(fontSize: 24),
)),
当Obx执行build之后,就开始创建Text对象,而Text构造函数入参是需要访问numberOneEntity.name,访问的过程实际就是取name的值,name是_RxImpl类型的,实际的值存储在_value里,因此取name的值就是取_value的值,而_RxImpl定义了_value 的get方法:
T get value {
RxInterface.proxy?.addListener(subject);
return _value;
}
get方法除了返回obs的具体值,还多了一个addListener的操作。
RxInterface.proxy 的值就是Obx State里的_observer,而此处的subject是_RxImpl里的成员变量,它是GetStream类型的。
void addListener(GetStream<T> rxGetx) {
//当前的对象是Obx里的_observer
//_subscriptions是该Obx维护的数组,因此一个Obx下面可能监听多个obs
//每个obs可能会触发多次get,而后触发addListener,此处做过滤
if (!_subscriptions.containsKey(rxGetx)) {
//重点是这
//rxGetx 是obs的成员变量,它的内部订阅了观察,和Obx的订阅流程类似
final subs = rxGetx.listen((data) {
//当obs改变时,会调用此方法,而subject是属于Obx里的
if (!subject.isClosed) subject.add(data);
});
final listSubscriptions =
_subscriptions[rxGetx] ??= <StreamSubscription>[];
listSubscriptions.add(subs);
}
}
Obx(观察者)和obs(被观察者),两者的连接是在addListener方法里。
继续看subject.add(data)
void add(T event) {
assert(!isClosed, 'You cannot add event to closed Stream');
_value = event;
_notifyData(event);
}
void _notifyData(T data) {
_isBusy = true;
for (final item in _onData!) {
if (!item.isPaused) {
item._data?.call(data);
}
}
_isBusy = false;
}
_onData是订阅的数组,里面存放的是观察者的回调,而对于Obx来说,其实存放的就是_updateTree。
因此,上述的逻辑可以归纳为:
obs监听了内部被观察者,当obs的回调被触发后,它会调用Obx的被观察者,而Obx的被观察者会调用其观察者,最后setState
现在就只差最后一个环节:obs的被观察者是什么时候触发的?
再来看刷新排行榜的操作:
ElevatedButton(
child: Text('刷新排行榜'),
onPressed: () {
numberOneEntity.name.value = "fish${Random().nextInt(100)}";
},
),
当点击按钮后,会给obs赋值,那么来追踪一下赋值过程,依然是在_RxImpl里。
set value(T val) {
if (subject.isClosed) return;
sentToStream = false;
if (_value == val && !firstRebuild) return;
firstRebuild = false;
_value = val;
sentToStream = true;
subject.add(_value);
}
除了传统的赋值(给obs里的值_value赋值),还多了一些其它操作,其中重要的是subject.add(_value)。
前面分析过Obx里的subject.add(data),而此时的subject(被观察者)是obs里的对象,两者的目的都是一样的:
通知各自的观察者
subject.add(_value)最后会调用到obs rxGetx.listen((data) 的闭包里:
final subs = rxGetx.listen((data) {
//当obs改变时,会调用此方法,而subject是属于Obx里的
if (!subject.isClosed) subject.add(data);
});
也即是通知了Obx的被观察者,Obx的被观察者再通知自己的观察者,最终setState,如此一来就完成了:
数据变更,触发UI刷新的流程
Obx、obs源码看起来比较绕的原因是它俩都依赖于NotifyManager,而NotifyManager里又封装了观察者模式,用一张图展示整体的流程:
红色部分是Obx和obs连接的关键点。
总体来看:
- Obx作为观察者,观察obs的变化
- 一个Obx里可以观察多个obs
- 一个obs可以被多个Obx观察
5. get 使用建议
第1点:Obx观察最小化原则
如上图,只需要观察第一个Text。
如上图,为了方便,包裹了整个Column,此种做法不推荐,理由是当name发生变化时会引发Obx的重建,最终第二个Text也会重建,浪费渲染时间和资源。
第2点:
GetX、GetBuilder 比较少用,Obx+obs组合可以替代它们。
第3点:
GetxController 并不是必须的,通常和GetView、GetBuilder搭配使用。
第4点:
和第一点类似,Obx不建议嵌套Obx。
好了,状态管理就讲到这里了。
你是否也在使用get呢?还是其它的框架?亦或是setState一把梭?
说说你遇到的坑吧~