一文看尽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 方法中暴露。

相关推荐
江上清风山间明月1 天前
Flutter开发的应用页面非常多时如何高效管理路由
android·flutter·路由·页面管理·routes·ongenerateroute
Zsnoin能2 天前
flutter国际化、主题配置、视频播放器UI、扫码功能、水波纹问题
flutter
早起的年轻人2 天前
Flutter CupertinoNavigationBar iOS 风格导航栏的组件
flutter·ios
HappyAcmen2 天前
关于Flutter前端面试题及其答案解析
前端·flutter
coooliang2 天前
Flutter 中的单例模式
javascript·flutter·单例模式
coooliang2 天前
Flutter项目中设置安卓启动页
android·flutter
JIngles1232 天前
flutter将utf-8编码的字节序列转换为中英文字符串
java·javascript·flutter
B.-2 天前
在 Flutter 中实现文件读写
开发语言·学习·flutter·android studio·xcode
freflying11192 天前
使用jenkins构建Android+Flutter项目依赖自动升级带来兼容性问题及Jenkins构建速度慢问题解决
android·flutter·jenkins
机器瓦力3 天前
Flutter应用开发:对象存储管理图片
flutter