likes
comments
collection
share

Flutter GetX 状态管理 使用总结

作者站长头像
站长
· 阅读数 36

前言

Flutter是基于声明式构建UI,我们只需要专注于处理好状态即可。但声明式会有以下几个问题:

  1. 逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等
  2. 难以跨组件 (跨页面) 访问数据
  3. 无法轻松的控制刷新范围 (页面 setState 的变化会导致全局页面的变化)

为了解决以上问题,出现了各种状态管理框架。

目前较火的两个框架是ProviderGet

各有各的优缺点吧,本文主要是对GetX的一个使用总结。

使用的版本是:get 4.6.1

Get

Get实际是很多工具的汇总,包括状态管理依赖注入路由管理翻译等。 GetX则是其中的高性能状态管理框架

对于它的使用感受,我的第一印象就是,简单。但,并不是说就很容易。我们还是需要去认真考虑是否需要使用,以及在何种场景使用

它的状态管理可以分为这几种: GetBuilderGetXObxMixinBuilder

以及它们通用的Controller:GetXController

GetXController

大致说一下GetXController,这个并不复杂,其实就是对于变量、方法的控制器,常量也可以存储,也可以当做是Manager。

若是想要区分的更细一点,可以分为StateLogic两个类,一个放变量常量,一个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();
  }
}

变量可以是普通的声明方式,也可以转换成反应式变量

有什么不同呢?简单来说,假如需要使用GetXObx,那就需要使用反应式变量。反应式变量在改变时就会通知使用它们的地方,并实时局部刷新。且内部做了优化,若是变量与上一次比相同,那么则不会更新

反应式变量

如上,.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这些ListMap有的它也有

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

按照官方说法,这是最省钱的一种方式,消耗最小,使用简单。

其刷新方式类似于BlockProvider,即上文所说的需要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可选,有则在构建时也会同步初始化对应TGetXController

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中也有对应的initStatedispose等方法,就相当于是让一个小组件可以自己管理自己的状态。且这个小组件可以是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,它可以更精细的控制不同变量对于屏幕的改变,以及范围。

但你可能就要对于不同的模块写不同的GetXGetXController,更精细写起来也就会更复杂一点。

性能方面,若是只更新某个小组件的状态,那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之中。在使用的时候,通过GetBuilderGetX也可以,Get.find也可以,找到对应的GetXController就可以控制和刷新对应的状态

并且Get有个很方便的就是,它所有东西都不需要依赖Context。所以他作为全局的状态管理来使用就很舒服。

页面的状态管理

也就是说只与单个页面或者少数几个关联页面绑定的GetXController,这个Controller仅服务于它们,并且跟随页面的创建而创建,释放而释放

就比如下面这种

Flutter GetX 状态管理 使用总结

页面所有的常量变量在state中管理,logic进行控制,view层只处理UI。

但也会相应的存在几个问题:

  1. 需要绑定路由的生命周期并且配置的话,需要同时使用Get的路由管理。且生命周期的处理,比如初始化释放都要考虑使用Get的方式。
  2. GetXController中的变量在释放前都是共有的,假如说打开使用相同Controller的页面,比如打开两个商详,它的状态会被继承,需要考虑是否要继承,或者说怎样去隔离。

第一点,会导致我们的写法与之前的可能有很大的改变,不仅仅是页面的构建,区分为state、logic、view,使用GetBuilder、GetX、Obx来精细控制。还需要考虑路由上状态的传递,需要结合GetRoute的路由,因为组件入参的方式,Get拿不到,就得手动设置给GetXController。那使用Get.to(page, arguments)的传递才合理。

第二点,要考虑动态的设置GetXControllertag,比如商详,使用商品id来做不同的区分,不同tagGetXController可以各自管理自己的状态。子组件也要记得传递tagGet.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
评论
请登录