Flutter GetX 状态管理 使用总结
前言
Flutter是基于声明式构建UI,我们只需要专注于处理好状态即可。但声明式会有以下几个问题:
- 逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等
- 难以跨组件 (跨页面) 访问数据
- 无法轻松的控制刷新范围 (页面 setState 的变化会导致全局页面的变化)
为了解决以上问题,出现了各种状态管理框架。
各有各的优缺点吧,本文主要是对GetX的一个使用总结。
使用的版本是:get 4.6.1
Get
Get实际是很多工具的汇总,包括状态管理、依赖注入、路由管理、翻译等。 GetX则是其中的高性能状态管理框架。
对于它的使用感受,我的第一印象就是,简单。但,并不是说就很容易。我们还是需要去认真考虑是否需要使用,以及在何种场景使用。
它的状态管理可以分为这几种: GetBuilder、GetX、Obx、MixinBuilder
以及它们通用的Controller:GetXController
GetXController
大致说一下GetXController,这个并不复杂,其实就是对于变量、方法的控制器,常量也可以存储,也可以当做是Manager。
若是想要区分的更细一点,可以分为State和Logic两个类,一个放变量常量,一个Function控制。
示例:
class LocaleGet extends GetxController {
Rx<Locale?> locale = const Locale('en').obs;
void initial() {
final site = SiteManager.getInstance().site;
if (site != null) {
locale(Locale(site.selectedLanguage!.configCode));
}
}
void changeLocale(Locale locale) {
this.locale(locale);
update();
}
}
变量可以是普通的声明方式,也可以转换成反应式变量。
有什么不同呢?简单来说,假如需要使用GetX和Obx,那就需要使用反应式变量。反应式变量在改变时就会通知使用它们的地方,并实时局部刷新。且内部做了优化,若是变量与上一次比相同,那么则不会更新。
反应式变量
如上,.obs
即可将变量转变为反应式变量,支持几乎所有类型和对象,转换后的类型为Rx<T>
。
它们的赋值取值方式根据T
类型会有些不同:
常见类型,如int, String, bool
取值赋值需要.value
,因为GetX不会破坏原有代码,所以不会影响原来的类型。
var count = 0.obs;
var open = true.obs;
count++;
// count = 1
print("count = ${count.value}");
open.value = !open.value;
// open = false
print("open = &{open.value}");
对象
对象较为特殊,赋值需要object()
,或者object.update()
;
/// 简单的方式,变量名带括号,括号中为需要更改的值
locale(newLocalge);
/// update方式,若赋值null则需要这种方式,不过最好不要用null来做处理
locale.update( (locale) {
locale.languageCode = 'en';
)};
取值:
// 注意变量加括号,l不是大写
locale().languageCode;
// 或.value
locale.value.languageCode;
List和Map
赋值取值和对象差不多,然后如.length
这些List
、Map
有的它也有
final list = List<User>().obs;
int length = list.length;
list.map((e) => e.id);
// .value
list.value = newList;
// 或object()
list(newList);
普通变量 -- update()
而普通的变量,则需要使用update()
来主动更新,这个跟Provider差不多。
在调用update()
之后,可以通知指定的GetBuilder进行更新
int count = 0;
void increase() {
count++;
// 通知更新
update();
}
update()
不仅可以更新普通变量,反应式变量也可以用它来通知GetBuilder更新
onInit()、onClose()等生命周期方法。
可以做与UI层的完全解耦,变量的初始化和释放都可以在Controller中进行。业务方法实现和逻辑也都在Controller中。
不过实际使用时并没有想象中这么美好。
GetXController的初始化创建方式,以及获取方法
/// 构建并初始化一个GetXController
Get.put(LocaleGet());
/// 自动根据上下文找到最近对应的GetXController,不需要BuildContext
Get.find<LocaleGet()>();
简单来说就是put
把Controller推入它的栈中,在需要使用时使用find
来找到栈中对应的。具体可以看看官方文档。
GetXController了解到这就可以正常使用了。
GetBuilder
按照官方说法,这是最省钱的一种方式,消耗最小,使用简单。
其刷新方式类似于Block和Provider,即上文所说的需要update()
主动刷新。使用起来手感跟Provider差不多。
child: GetBuilder<LocaleGet>(
init: LocaleGet(),
builder: (logic) {
...
}
)
/// const GetBuilder({
/// Key? key,
/// this.init,
/// this.global = true,
/// required this.builder,
/// this.autoRemove = true,
/// this.assignId = false,
/// this.initState,
/// this.filter,
/// this.tag,
/// this.dispose,
/// this.id,
/// this.didChangeDependencies,
/// this.didUpdateWidget,
/// }) : super(key: key);
init
可选,有则在构建时也会同步初始化对应T
的GetXController。
builder
下面为我们的UI,返回的logic
为初始化完成的GetXController,内部其实就是通过find
的形式找到的,跟我们自己Get.find
是一样的。
// 创建好的GetXController都会在GetInstance()中
var isRegistered = GetInstance().isRegistered<T>(tag: widget.tag);
if (widget.global) {
if (isRegistered) {
if (GetInstance().isPrepared<T>(tag: widget.tag)) {
_isCreator = true;
} else {
_isCreator = false;
}
controller = GetInstance().find<T>(tag: widget.tag);
} else {
controller = widget.init;
_isCreator = true;
GetInstance().put<T>(controller!, tag: widget.tag);
}
} else {
controller = widget.init;
_isCreator = true;
controller?.onStart();
}
在GetBuilder中也有对应的initState、dispose等方法,就相当于是让一个小组件可以自己管理自己的状态。且这个小组件可以是StatelessWidget。
理想状态下,可以将页面的StatefullWidget都转变为StatelessWidget,用GetBuilder来做状态管理,除非是需要TickerProviderStateMixin这一类的操作。
实际上我们看GetBuilder的源码,它也就是StatefullWidget,一个简单的状态管理器,所以需要我们主动的告知它去更新。不过它比Provider好的一点是,它可以通过id来指定哪几个Builder去更新。
update(['1', '2']);
基本上可以认为update()
是GetBuilder专用的更新方法。
GetX
他的用法跟GetBuilder很像,但,它是专用于反应式的。就是说它不用update()
。
GetX<Controller>(
builder: (logic) {
return Text('${logic.text.value}');
},
)
// const GetX({
// this.tag,
// required this.builder,
// this.global = true,
// this.autoRemove = true,
// this.initState,
// this.assignId = false,
// // this.stream,
// this.dispose,
// this.didChangeDependencies,
// this.didUpdateWidget,
// this.init,
// // this.streamController
// });
用法很简单,在GetXController中的反应式变量更新时,对应的GetX就会更新自己。对比GetBuilder,它可以更精细的控制不同变量对于屏幕的改变,以及范围。
但你可能就要对于不同的模块写不同的GetX和GetXController,更精细写起来也就会更复杂一点。
性能方面,若是只更新某个小组件的状态,那GetX有更好的性能;若是更新几乎整个页面或者几乎所有的变量,那GetBuilder会更好。也可以结合使用。
GetX在状态有错误使用时,可以更好的处理屏幕的展示。简单来说就是状态报错了,你的页面不会报红(release变灰)。这点是比Obx好的地方。
Obx模式
Obx是更经济的反应式组件,也就是写法上更加简洁方便。它只有一个builder
,但他使用上会比GetX更加严格(动不动就给你报红)。在Obx()
中,必须能够监测到至少一个成功初始化好的反应式变量,否则就会报错提示不能使用。
并且,Obx不能够嵌套Obx,且这个限制不管你是否抽离了父子组件,然而父组件的Obx控制不到子组件中的状态,或者说是不好控制。
严格的程度比如说,条件判断,true
使用了反应式变量,false
没有,那false
时就会报错。就是说反应式变量必须插入到Tree中可以被它找到。
Obx(() {
return Container(
// 会报错 SizedBox 中没有反应式变量
child: show ? SizedBox() : Text(
'${controller.text.value}'
),
);
});
/// 需要调整为
show ? SizedBox() : Obx(() {
return Container(
child: Text(
'${controller.text.value}'
),
);
});
写法上很简单,只用考虑是否有对应的反应式变量需要更新,并且对应的反应式变量更新会更新对应的Obx()。局部更新更加精细,需要精细到最终应用的层。
与GetX相比,它可以监听多个GetXController的状态改变,GetX只能绑定一个。因此它代码会更加简洁。
MixinBuilder
这个东西就更倾向于与页面结合使用,比较复杂,是混合GetBuilder的update模式和反应式模式。
class Controller extends GetController with StateMixin<User>{}
页面状态更新及支持的状态
change(data, status: RxStatus.success());
RxStatus.loading();
RxStatus.success();
RxStatus.empty();
RxStatus.error('message');
官方示例的页面
class OtherClass extends GetView<Controller> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: controller.obx(
(state)=>Text(state.name),
// here you can put your custom loading indicator, but
// by default would be Center(child:CircularProgressIndicator())
onLoading: CustomLoadingIndicator(),
onEmpty: Text('No data found'),
// here also you can set your own error widget, but by
// default will be an Center(child:Text(error))
onError: (error)=>Text(error),
),
);
}
其实就是GetView方式,我基本没用到,因为感觉不太“自由”,有点麻烦了。
全局的状态管理
实际中常用的,就是作为全局,跨页面的状态管理使用。较于Provider使用更加简单直接。
全局的GetXController的初始化时机,在App UI构建的最开始就可以了,比如main的build之中。在使用的时候,通过GetBuilder、GetX也可以,Get.find
也可以,找到对应的GetXController就可以控制和刷新对应的状态。
并且Get有个很方便的就是,它所有东西都不需要依赖Context。所以他作为全局的状态管理来使用就很舒服。
页面的状态管理
也就是说只与单个页面或者少数几个关联页面绑定的GetXController,这个Controller仅服务于它们,并且跟随页面的创建而创建,释放而释放。
就比如下面这种
页面所有的常量变量在state中管理,logic进行控制,view层只处理UI。
但也会相应的存在几个问题:
- 需要绑定路由的生命周期并且配置的话,需要同时使用Get的路由管理。且生命周期的处理,比如初始化释放都要考虑使用Get的方式。
- GetXController中的变量在释放前都是共有的,假如说打开使用相同Controller的页面,比如打开两个商详,它的状态会被继承,需要考虑是否要继承,或者说怎样去隔离。
第一点,会导致我们的写法与之前的可能有很大的改变,不仅仅是页面的构建,区分为state、logic、view,使用GetBuilder、GetX、Obx来精细控制。还需要考虑路由上状态的传递,需要结合GetRoute的路由,因为组件入参的方式,Get拿不到,就得手动设置给GetXController。那使用Get.to(page, arguments)
的传递才合理。
第二点,要考虑动态的设置GetXController的tag,比如商详,使用商品id来做不同的区分,不同tag的GetXController可以各自管理自己的状态。子组件也要记得传递tag,Get.find
找到对应的Controller。还是需要根据不同情况考虑使用。
或者,也可以尝试用Uuid
,或者路由计数来保证每个页面都是新的GetXController,相互隔离。相对来说tag的传递会复杂一点,除非子组件状态都由父组件来管理。
void initGetX(V getXController, [String? tag]) {
if (!_getXInit) {
if (tag == null && _getXTag == null) {
// 获取对应路由计数
int currentTimes = GetPageManager.instance.getCurrentPageTimes() ?? 1;
if (currentTimes <= 1) {
_getXTag = Get.currentRoute;
} else {
_getXTag = "${Get.currentRoute}$currentTimes";
}
} else {
_getXTag = tag;
}
controller = Get.put<V>(getXController, tag: _getXTag);
_getXInit = true;
}
}
总结
在熟悉之后,使用GetX来做全局的状态管理,还是很推荐的,使用起来又简单,也不难理解。并且普通全局状态的更新,经常的导致大量不必要的rebuild,可以通过GetX来控制更新的范围,而不需要setState
更新整个页面整个App。
假如说刚开始就考虑使用GetX来搭建页面,那可以考虑页面绑定GetXController,从0开始,不论是路由还是GetXController的tag都可以更好的根据具体情况来设计。
假如是后面改用GetX,那么则不建议页面结合GetX结合路由的形式,改造成本会比较大,甚至原有逻辑都需要打破重新设计。仅结合页面不结合路由的话,变量还需要手动设置到GetXController中,很麻烦,不如不用。
可以根据具体情况,对于需要状态管理的做状态管理,感觉会更好。
转载自:https://juejin.cn/post/7101981143901143047