一文看尽Get库——Flutter状态管理利器

Get一词一语双关,既点明这个库解决的痛点,可以简单快捷地获取项目中的状态;

同时也向大家发出邀请,仿佛在明示开发者,当你Get到Get库的使用后,你就能灵活运用Flutter项目中的状态管理。

官方中文文档:

github.com/jonataslaw/...

一、Flutter的状态管理

1.1 什么是状态?

在Flutter项目中,状态管理是一个永恒的话题。那到底什么是状态呢?

答曰:一个应用的状态就是当这个应用运行时存在于内存中的所有内容。当然,在这个"所有内容"中,Flutter框架本身会负责管理许多内容。对于开发者来说,真正需要进行状态管理的一般只有"重建页面时所需要的数据"。

1.2 为什么需要状态管理工具?

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

(1)逻辑和页面 UI 耦合,导致无法复用/单元测试、修改混乱等

(2)难以跨组件 (跨页面) 访问数据

(3)无法轻松地控制刷新范围 (页面 setState 的变化会导致全局页面的变化)

然而实际上,为了提高代码可维护性和可读性,开发者应该尽量遵循业务逻辑和页面UI分开设计的原则。

为了简单快捷地实现这样的目标,减少开发者的负担并提高开发效率,很多负责状态管理的开源库出现了。

1.3 状态管理工具

在第三方库中,目前应用最多的状态管理库当属Get和Provider。

先来感受一下两个库的统计数据,包括点赞数和Links数。

从上面的数据可以看出,Get库的受欢迎程度"遥遥领先"。

但是,这不是说all in Get就完事了。原作者也在官方文档中写到

Get不是其他状态管理器的敌人。

Get是一个微框架,而不仅仅是一个状态管理器,它既可以单独使用,也可以与其他状态管理器结合使用。

目前在我们的项目中,Get和Provider是共存的。

关于Provider,我们团队的同学已经有过详细介绍,可以参考下面的文章:

Flutter状态管理之Provider

本篇文章则重点介绍Get库的状态管理功能。

二、Get库之初见

2.1 简单介绍

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

如果要使用Get,首先需要将 Get 添加到你的 pubspec.yaml 文件中。

vbnet 复制代码
dependencies:
  get: 4.6.5

接着在需要用到的文件中导入即可。

arduino 复制代码
import 'package:get/get.dart';

2.2 Get的三大主要功能

(1)状态管理:Get有两个不同的状态管理器:简单的状态管理器(GetBuilder)和响应式状态管理器(GetX)。

(2)路由管理:Get路由管理的最大优势之一就是不需要context,且使用方法极其简单。

(3)依赖管理:Get有一个简单而强大的依赖管理器,可以很方便地检索到控制器,无需Provider context和InheritedWidget。

2.3 为什么选择GetX?

在状态管理工具中,BLoc 、Provider 等均使用了context,需要通过上下文来寻找InheritedWidget,这种解决方案限制了状态管理必须在父子关系的 widget 树中,业务逻辑也会对 View 产生较强的依赖。

而 GetX 则不需要上下文,突破了InheritedWidget的限制,我们可以在全局和模块间共享状态,这正是 BLoc 、Provider 等框架的短板。

三、Get库的状态管理

3.1 简单状态管理器

(1)简单用法示例

首先创建控制器类并继承GetxController。

scala 复制代码
class Controller extends GetxController {
  int counter = 0;
  void increment() {
    counter++;
    update(); // 当调用增量时,使用update()来更新用户界面上的计数器变量。
  }
}

接着在Stateless/Stateful类中,使用GetBuilder包裹需要更新的组件。当调用increment时,update会通知GetBuilder进行更新。

less 复制代码
GetBuilder<Controller>(
  init: Controller(), // 首次启动
  builder: (s) => Text('${s.counter}'),
)

// 注意:init参数只在第一次使用时用来初始化控制器。
// 如果使用Get.put()方法提前注册控制器,则不需要使用init参数。
// Get.put()方法的具体使用见4.2节。

(2)如果需要使用之前的Controller中的数据,只需要再用一次GetBuilder(没有init)。

scala 复制代码
class OtherClass extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: GetBuilder<Controller>(
          builder: (s) => Text('${s.counter}'),
        ),
      ),
    );
  }

(3)如果需要在其他地方使用控制器,但是无法调用GetBuilder时,那么有两种方法可以实现。

scala 复制代码
方法1:

// 首先在控制器中创建一个get方法
class Controller extends GetxController {
  // 添加这一行
  static Controller get to => Get.find(); 

  int counter = 0;
  void increment() {
    counter++;
    update();
  }
}

// 然后可以直接访问控制器
FloatingActionButton(
  onPressed: () {
    Controller.to.increment(),
  }
  child: Text("${Controller.to.counter}"),
),
scss 复制代码
方法2:

// 直接使用Get.find方法来访问控制器
FloatingActionButton(
  onPressed: () {
    Get.find<Controller>().increment();
  }
  child: Text("${Get.find<Controller>().counter}"),
),

当按下FloatingActionButton时,所有监听'counter'变量的widget都会自动更新。

另外,使用以上两种语法在性能上没有区别,也没有任何副作用。

区别仅在于一个不需要类型,另一个IDE会自动完成。

(4)update方法可以进行局部更新,多种状态分别更新,不需要为每个状态创建一个类

下面的两个方法分别改变两个变量,在update()中添加了 id ,这样就只更新这个 id 对应的GetBuilder:

ini 复制代码
int _counter = 0;
int get counter => _counter;

String _name = "Lili";
String get firstName => _name;

  void increment() {
  _counter++;
  _name = WordPair.random().asPascalCase;
  update(['counter']);
}

void changeName() {
  _counter++;
  _name = WordPair.random().asPascalCase;
  update(['name']);
}
less 复制代码
GetBuilder<Controller>(
  id: 'counter',
  builder: (s) => Text(s.counter.toString()),
),
SizedBox(
  height: 50,
),
GetBuilder<Controller>(
  id: 'name',
  builder: (s) => Text(s.firstName),
),

(5)简单状态管理器的优点

  1. 可以用GetBuilder只包裹需要更新的最小部件。
  2. 不使用changeNotifier,状态管理器使用较少的内存(接近0mb)。
  3. 真正的解耦项目,控制器和UI完全分离设计。
  4. 更新widgets而不需要为此花费RAM。Get只存储GetBuilder的创建者ID,必要时更新该GetBuilder。Get ID存储在内存中的消耗非常低,即使是成千上万的GetBuilders。
  5. Get是全知全能的。在大多数情况下,它很清楚地知道从内存中取出一个控制器的时机,使用者不需要担心什么时候需要移除一个控制器,Get知道最佳的时机。

3.2 响应式状态管理器

在响应式编程中,可以为数据创建StreamControllers ,然后以流的方式发送数据。GetX可以实现同样的功能,并且实现起来非常简单。

(1)用法说明

为变量添加一个后缀".obs"就可以使其变得可观察。变量每次改变的时候,使用它的小部件就会被更新。

ini 复制代码
// before
var name = 'Jonatas Borges';
// now
var name = 'Jonatas Borges'.obs;

然后通过 Obx 或者 GetX 包裹并使用响应式变量的控件,那么在变量改变的时候就会被更新

scss 复制代码
Obx (() => Text (Get.find<Controller>().name));
或
GetX<Controller>(
  builder: (s) {
    return Text(s.name);
  },
),

除了直接添加后缀外,还有两种方法可以把一个变量变成是 "可观察的"。

ini 复制代码
方法2: 使用 Rx{Type}
final name = RxString('');
final isLogged = RxBool(false);
final count = RxInt(0);

方法3: 使用 Rx,规定泛型 Rx<Type>
final name = Rx<String>('');
final isLogged = Rx<Bool>(false);
final count = Rx<Int>(0);

(2)简单示例

scala 复制代码
// controller
class Controller extends GetxController {
  final count1 = 0.obs;
  final count2 = 0.obs;
  int get sum => count1.value + count2.value;
}

// view
// .obs实现了一个被观察者,它们不再是int类型,而是RxInt类型。
// 对应的小部件也不再是GetBuilder,而是下面两种
GetX<Controller>(
  builder: (s) {
    return Text('${s.count1.value}');
  },
),

Obx(() => Text('${Get.find<Controller>().count2.value}')),

因为是响应式,不再需要调用update方法,每次更改值,调用值的地方都会自动刷新。

更神奇的是,他们的运算和也是响应式的,如下

scss 复制代码
GetX<Controller>(
  builder: (s) {
    return Text('${s.sum}');
  },
),
或
Obx(() => Text('${Get.find<Controller>().sum}')),

只要更新count1或者count2,那么使用sum的小部件也会随之更新。

(3)优点

  1. 不需要创建StreamControllers
  2. 不需要为每个变量创建一个StreamBuilder
  3. 可以对更新的内容进行精细的控制

3.3 总结

简单状态管理器

1、创建Controller类并继承GetxController

2、使用Get.put()注册Controller实例

3、构造GetBuilder方法将View和Controller关联起来

4、在GetBuilder方法中初始化Controller或者使用已经注册过的Controller实例

5、对于Controller类,记得在需要通知更新的方法中添加update()

响应式状态管理器

1、创建Controller类并继承GetxController

2、为变量添加一个后缀".obs"使其变为可观察变量

3、使用Get.put()注册Controller实例

4、使用 Obx 或者 GetX 组件,在组件内调用需要的变量

5、响应式状态管理,不需要调用update方法

四、Get库的其他功能

Get的路由管理和依赖管理往往需要配合状态管理来使用,下面对这两块内容进行简单介绍。

4.1 路由管理

首先,在MaterialApp前加上 "Get",把它变成GetMaterialApp

less 复制代码
GetMaterialApp( // Before: MaterialApp(
  home: MyHome(),
)

接下来就可以使用Get来进行导航,下面给出几个常用的例子。详细指南请见:Get在路由管理方面的完整说明

vbnet 复制代码
1、导航到新页面
Get.to(NextScreen());

2、关闭任何通常会用Navigator.pop(context)关闭的东西
Get.back();

3、进入下一个页面,但没有返回上一个页面的选项(用于闪屏页,登录页面等)
Get.off(NextScreen());

4、进入下一个页面并取消之前的所有路由
Get.offAll(NextScreen());

4.2 依赖管理

Get的依赖管理器可以只用1行代码就检索到与Controller相同的类,而无需Provider context,无需inheritedWidget。

在Get实例中实例化控制器,这将使它在整个App中可用,如下。

ini 复制代码
Controller controller = Get.put(Controller()); 

(1)Get.put方法

less 复制代码
Get.put<SomeClass>(SomeClass());
Get.put<LoginController>(LoginController(), permanent: true);
Get.put<ListItemController>(ListItemController, tag: "some unique string");

该方法的所有参数说明如下:

arduino 复制代码
Get.put<S>(
  // 必备:你想得到保存的类,比如控制器或其他东西。
  // 注:"S "意味着它可以是任何类型的类。
  S dependency

  // 可选:当你想要多个相同类型的类时,可以用这个方法。
  // 因为你通常使用Get.find<Controller>()来获取一个类。
  // 你需要使用标签来告诉你需要哪个实例。
  // 必须是唯一的字符串
  String tag,

  // 可选:默认情况下,get会在实例不再使用后进行销毁
  // (例如:一个已经销毁的视图的Controller)
  // 但你可能需要这个实例在整个应用生命周期中保留在那里,就像一个sharedPreferences的实例或其他东西。
  // 默认值为false
  bool permanent = false,

  // 可选:允许你在测试中使用一个抽象类后,用另一个抽象类代替它,然后再进行测试。
  // 默认为false
  bool overrideAbstract = false,

  // 可选:允许你使用函数而不是依赖(dependency)本身来创建依赖。
  // 这个不常用
  InstanceBuilderCallback<S> builder,
)

(2)Get.lazyPut方法

该方法可以懒加载一个依赖,这样它只有在使用时才会被实例化。

scss 复制代码
/// 只有当第一次使用Get.find<ApiMock>时,ApiMock才会被调用。
Get.lazyPut<ApiMock>(() => ApiMock());

Get.lazyPut<FirebaseAuth>(
  () {
    // ... 
    return FirebaseAuth();
  },
  tag: Math.random().toString(),
  fenix: true
)

Get.lazyPut<Controller>( () => Controller() )

该方法的可选参数说明如下

arduino 复制代码
Get.lazyPut<S>(
  // 强制性:当你的类第一次被调用时,将被执行的方法。
  InstanceBuilderCallback builder,
  
  // 可选:和Get.put()一样,当你想让同一个类有多个不同的实例时,就会用到它。
  // 必须是唯一的
  String tag,

  // 可选:类似于 "永久",
  // 不同的是,当不使用时,实例会被丢弃,但当再次需要使用时,Get会重新创建实例,
  // 默认值为false
  bool fenix = false
)

(3)Get.find使用已有的控制器

可以采用下面两种方法来获得对应的控制器

ini 复制代码
final controller = Get.find<Controller>();
// 或
Controller controller = Get.find();

// 然后访问控制器的数据
Text(controller.textFromApi);

五、状态管理库对比

在1.2节中,给出了当前flutter中存在的三个问题。这一节将从这三个方面的解决思路上对Provider和Get进行对比。

5.1 逻辑和页面UI耦合问题

这个问题可以通过 MVP 模式进行解耦。简单来说就是将 View 中的逻辑代码抽离到 Presenter 层, View 只负责视图的构建,Model负责管理存储数据。

这也是 Flutter 中几乎所有状态管理框架的解决思路,图中的 Presenter 可以认为是 Get 中的 GetController、 Provider 中的 ChangeNotifier 或者 Bloc 中的 Bloc。

对于这两个状态管理工具来说,它们的解决思路不一样。

  • Provider通过 Flutter 树机制 解决

在Flutter中,可以通过 context.findAncestorStateOfType 一层一层地向上查找到需要的 Element 对象,获取 Widget 或者 State 后即可取出需要的变量。

Provider 也是借助了这样的机制,完成了 View -> Presenter 的获取。通过 Provider.of 获取顶层 Provider 组件中的 Present 对象。显然,所有 Provider 以下的 Widget 节点,都可以通过自身的 context 访问到 Provider 中的 Presenter,很好地解决了跨组件的通信问题。

  • Get通过 依赖注入 解决

树机制很不错,但依赖于 context,这一点有时很让人抓狂。我们知道 Dart 是一种单线程的模型,所以不存在多线程下对于对象访问的竞态问题。基于此 Get 借助一个全局单例的 Map 存储对象。通过依赖注入的方式,实现了对 Presenter 层的获取。这样在任意的类中都可以获取到 Presenter。

这个 Map 对应的 key 是 runtimeType + tag,其中 tag 是可选参数,而 value 对应 Object,也就是说我们可以存入任何类型的对象,并且在任意位置获取。

5.2 难以跨组件 (跨页面) 访问数据的问题

这个问题其实和上一部分的思考基本类似,我们可以总结一下两种方案特点:

Provider

  • 依赖树机制,必须基于 context
  • 提供了子组件访问上层的能力

Get

  • 全局单例,任意位置可以存取
  • 存在类型重复,内存回收问题

5.3 高层级 setState 引起不必要刷新的问题

Flutter 通过采用观察者模式解决,其关键在于两步:

  1. 观察者去订阅被观察的对象;
  2. 被观察的对象通知观察者。

Provider 中提供了 ChangeNotifierProvider 来实现相应功能。

在 Get 中,则只需要提前调用 Get.put 方法存储控制器对象,然后使用 GetBuilder 就能获取到存入的对象,并在 builder 方法中暴露。

相关推荐
Python私教2 小时前
macOS 中搭建 Flutter 开发环境
flutter·macos
明似水6 小时前
掌握 Flutter 中的 `Overlay` 和 `OverlayEntry`:弹窗管理的艺术
javascript·flutter
Flutter社区17 小时前
使用 Flutter 3.19 更高效地开发
flutter·dart
Forever不止如此19 小时前
【CustomPainter】绘制圆环
flutter·custompainter·圆环
wills77720 小时前
Flutter Error: Type ‘UnmodifiableUint8ListView‘ not found
flutter
AiFlutter2 天前
Flutter之Package教程
flutter
Mingyueyixi2 天前
Flutter Spacer引发的The ParentDataWidget Expanded(flex: 1) 惨案
前端·flutter
crasowas2 天前
Flutter问题记录 - 适配Xcode 16和iOS 18
flutter·ios·xcode
老田低代码3 天前
Dart自从引入null check后写Flutter App总有一种难受的感觉
前端·flutter