Get一词一语双关,既点明这个库解决的痛点,可以简单快捷地获取项目中的状态;
同时也向大家发出邀请,仿佛在明示开发者,当你Get到Get库的使用后,你就能灵活运用Flutter项目中的状态管理。
官方中文文档:
一、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,我们团队的同学已经有过详细介绍,可以参考下面的文章:
本篇文章则重点介绍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)简单状态管理器的优点
- 可以用GetBuilder只包裹需要更新的最小部件。
- 不使用changeNotifier,状态管理器使用较少的内存(接近0mb)。
- 真正的解耦项目,控制器和UI完全分离设计。
- 更新widgets而不需要为此花费RAM。Get只存储GetBuilder的创建者ID,必要时更新该GetBuilder。Get ID存储在内存中的消耗非常低,即使是成千上万的GetBuilders。
- 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)优点
- 不需要创建StreamControllers
- 不需要为每个变量创建一个StreamBuilder
- 可以对更新的内容进行精细的控制
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 通过采用观察者模式解决,其关键在于两步:
- 观察者去订阅被观察的对象;
- 被观察的对象通知观察者。
Provider 中提供了 ChangeNotifierProvider 来实现相应功能。
在 Get 中,则只需要提前调用 Get.put 方法存储控制器对象,然后使用 GetBuilder 就能获取到存入的对象,并在 builder 方法中暴露。