Flutter GetX - 状态管理
前言
Flutter的很多灵感来自于React,它的设计思想是数据与视图分离,由数据映射渲染视图。所以在Flutter中,它的Widget是immutable的,而它的动态部分全部放到了状态(State)中。于是状态管理自然便成了我们密切关注的对象。
为什么需要状态管理?
在我们一开始构建应用的时候,也许很简单。我们有一些状态,直接把他们映射成视图就可以了。这种简单应用可能并不需要状态管理。
但是随着功能的增加,你的应用程序将会有几十个甚至上百个状态。这个时候你的应用应该会是这样
Wow,这是什么鬼。我们很难再清楚的测试维护我们的状态,因为它看上去实在是太复杂了!而且还会有多个页面共享同一个状态,
例如当你进入一个文章点赞,退出到外部缩略展示的时候,外部也需要显示点赞数,这时候就需要同步这两个状态。
这时候,我们便迫切的需要一个架构来帮助我们理清这些关系,状态管理框架应运而生。
Getx状态管理
目前,Flutter有几种状态管理器。但是,它们中的大多数都涉及到使用ChangeNotifier来更新widget,这对于中大型应用的性能来说是一个糟糕的方法。
参考:
状态管理:https://github.com/jonataslaw/getx/blob/master/documentation/zh_CN/state_management.md
响应式状态管理器
响应式编程可能会让很多人感到陌生,因为它很复杂,但是GetX将响应式编程变得非常简单。
- 你不需要创建StreamControllers.
- 你不需要为每个变量创建一个StreamBuilder。
- 你不需要为每个状态创建一个类。
- 你不需要为一个初始值创建一个get。
让我们想象一下,你有一个名称变量,并且希望每次你改变它时,所有使用它的小组件都会自动刷新。
这是你的计数变量:
var name = 'Jonatas Borges';
要想让它变得可观察,你只需要在它的末尾加上".obs"。
var name = 'Jonatas Borges'.obs;
就这么简单!
我们把这个reactive-".obs"(ervables)变量称为_Rx_。
我们做了什么?我们创建了一个 "Stream "的 "String",分配了初始值 "Jonatas Borges",我们通知所有使用 "Jonatas Borges "的widgets,它们现在 "属于 "这个变量,当_Rx_的值发生变化时,它们也要随之改变。
这就是GetX的魔力,这要归功于Dart的能力。
但是,我们知道,一个Widget
只有在函数里面才能改变,因为静态类没有 "自动改变 "的能力。
你需要创建一个StreamBuilder
,订阅这个变量来监听变化,如果你想在同一个范围内改变几个变量,就需要创建一个 "级联 "的嵌套StreamBuilder
,对吧?
不,你不需要一个StreamBuilder
,但你对静态类的理解是对的。
在视图中,当我们想改变一个特定的Widget时,我们通常有很多Flutter方式的模板。 有了GetX,你也可以忘记这些模板代码了。
StreamBuilder( ... )
? initialValue: ...
? builder: ...
? 不,你只需要把这个变量放在Obx()
这个Widget里面就可以了。
Obx (() => Text (controller.name));
_你只需记住 Obx(()=>
你只需将Widget通过一个箭头函数传递给 Obx()
(_Rx_的 "观察者")。
Obx
是相当聪明的,只有当controller.name
的值发生变化时才会改变。
如果name
是"John"
,你把它改成了"John"
(name.value="John"
),因为它和之前的value
是一样的,所以界面上不会有任何变化,而Obx
为了节省资源,会直接忽略新的值,不重建Widget。这是不是很神奇?
那么,如果我在一个Obx
里有5个_Rx_(可观察的)变量呢?
当其中任何一个变量发生变化时,它就会更新。
如果我在一个类中有 30 个变量,当我更新其中一个变量时,它会更新该类中所有的变量吗?
不会,只会更新使用那个 Rx 变量的特定 Widget。
所以,只有当_Rx_变量的值发生变化时,GetX才会更新界面。
final isOpen = false.obs;
//什么都不会发生......相同的值。
void onButtonTap() => isOpen.value=false;
优势
当你需要对更新的内容进行精细的控制时,GetX() 可以帮助你。
如果你不需要 "unique IDs",比如当你执行一个操作时,你的所有变量都会被修改,那么就使用GetBuilder
。 因为它是一个简单的状态更新器(以块为单位,比如setState()
),只用几行代码就能完成。 它做得很简单,对CPU的影响最小,只是为了完成一个单一的目的(一个_State_ Rebuild),并尽可能地花费最少的资源。
如果你需要一个强大的状态管理器,用GetX是不会错的。
它不能和变量一起工作,除了__flows__,它里面的东西本质都是Streams
。 你可以将_rxDart_与它结合使用,因为所有的东西都是Streams
。 你可以监听每个"_Rx_变量 "的 "事件"。 因为里面的所有东西都是 "Streams"。
这实际上是一种_BLoC_方法,比_MobX_更容易,而且没有代码生成器或装饰。 你可以把任何东西变成一个_"Observable"_,只需要在它末尾加上.obs
。
最高性能
除了有一个智能的算法来实现最小化的重建,GetX还使用了比较器以确保状态已经改变。
如果你的应用程序中遇到错误,并发送重复的状态变更,GetX将确保它不会崩溃。
使用GetX,只有当value
改变时,状态才会改变。 这就是GetX,和使用MobX_的_computed
的主要区别。 当加入两个__observable__,其中一个发生变化时,该_observable_的监听器也会发生变化。
使用GetX,如果你连接了两个变量,GetX()
(类似于Observer()
)只有在它的状态真正变化时才会重建。
声明一个响应式变量
你有3种方法可以把一个变量变成是 "可观察的"。
第一种是使用 Rx{Type}
。
// 建议使用初始值,但不是强制性的
final name = RxString('');
final isLogged = RxBool(false);
final count = RxInt(0);
final balance = RxDouble(0.0);
final items = RxList<String>([]);
final myMap = RxMap<String, int>({});
第二种是使用 Rx,规定泛型 Rx<Type>。
final name = Rx<String>('');
final isLogged = Rx<Bool>(false);
final count = Rx<Int>(0);
final balance = Rx<Double>(0.0);
final number = Rx<Num>(0)
final items = Rx<List<String>>([]);
final myMap = Rx<Map<String, int>>({});
// 自定义类 - 可以是任何类
final user = Rx<User>();
第三种更实用、更简单、更可取的方法,只需添加 .obs 作为value的属性。
final name = ''.obs;
final isLogged = false.obs;
final count = 0.obs;
final balance = 0.0.obs;
final number = 0.obs;
final items = <String>[].obs;
final myMap = <String, int>{}.obs;
// 自定义类 - 可以是任何类
final user = User().obs;
有一个反应的状态,很容易。
我们知道,_Dart_现在正朝着_null safety_的方向发展。 为了做好准备,从现在开始,你应该总是用一个初始值来开始你的_Rx_变量。
用GetX将一个变量转化为一个_observable_ + _initial value_是最简单,也是最实用的方法。
你只需在变量的末尾添加一个".obs
",即可把它变成可观察的变量, 然后它的.value
就是_初始值_)。
使用视图中的值
// controller
final count1 = 0.obs;
final count2 = 0.obs;
int get sum => count1.value + count2.value;
// 视图
GetX<Controller>(
builder: (controller) {
print("count 1 rebuild");
return Text('${controller.count1.value}');
},
),
GetX<Controller>(
builder: (controller) {
print("count 2 rebuild");
return Text('${controller.count2.value}');
},
),
GetX<Controller>(
builder: (controller) {
print("count 3 rebuild");
return Text('${controller.sum}');
},
),
如果我们把count1.value++
递增,就会打印出来:
count 1 rebuild
count 3 rebuild
如果我们改变count2.value++
,就会打印出来。
count 2 rebuild
count 3 rebuild
因为count2.value
改变了,sum
的结果现在是2
。
- 注意:默认情况下,第一个事件将重建小组件,即使是相同的
值
。 这种行为是由于布尔变量而存在的。
想象一下你这样做。
var isLogged = false.obs;
然后,你检查用户是否 "登录",以触发ever
的事件。
@override
onInit(){
ever(isLogged, fireRoute);
isLogged.value = await Preferences.hasToken();
}
fireRoute(logged) {
if (logged) {
Get.off(Home());
} else {
Get.off(Login());
}
}
如果 "hasToken "是 "false","isLogged "就不会有任何变化,所以 "ever() "永远不会被调用。 为了避免这种问题,_observable_的第一次变化将总是触发一个事件,即使它包含相同的.value
。
如果你想删除这种行为,你可以使用: isLogged.firstRebuild = false;
。
什么时候重建
此外,Get还提供了精细的状态控制。你可以根据特定的条件对一个事件进行条件控制(比如将一个对象添加到List中)。
// 第一个参数:条件,必须返回true或false。
// 第二个参数:如果条件为真,则为新的值。
list.addIf(item < limit, item);
没有装饰,没有代码生成器,没有复杂的程序: 爽歪歪!
你知道Flutter的计数器应用吗?你的Controller类可能是这样的。
class CountController extends GetxController {
final count = 0.obs;
}
用一个简单的。
controller.count.value++
你可以在你的UI中更新计数器变量,不管它存储在哪里。
可以使用.obs的地方
你可以在 obs 上转换任何东西,这里有两种方法:
- 可以将你的类值转换为 obs
class RxUser {
final name = "Camila".obs;
final age = 18.obs;
}
- 或者可以将整个类转换为一个可观察的类。
class User {
User({String name, int age});
var name;
var age;
}
//实例化时。
final user = User(name: "Camila", age: 18).obs;
Obx
lib/pages/state_obx/index.dart
class StateObxView extends StatelessWidget {
StateObxView({Key? key}) : super(key: key);
final count = 0.obs;
@overrideWidget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Obx(...)"),
),
body: Center(
child: Column(
children: [
Obx(() => Text("count1 -> " + count.toString())),
Obx(() => Text("count2 -> " + count.toString())),
//Divider(),ElevatedButton(
onPressed: () {
count.value++;
},
child: Text('add'),
),
],
),
),
);
}
}
obs、extension、RxInt、Rx
...
extension StringExtension on String {
/// Returns a `RxString` with [this] `String` as initial value.
RxString get obs => RxString(this);
}
extension IntExtension on int {
/// Returns a `RxInt` with [this] `int` as initial value.
RxInt get obs => RxInt(this);
}
extension DoubleExtension on double {
/// Returns a `RxDouble` with [this] `double` as initial value.
RxDouble get obs => RxDouble(this);
}
extension BoolExtension on bool {
/// Returns a `RxBool` with [this] `bool` as initial value.
RxBool get obs => RxBool(this);
}
extension RxT<T> on T {
/// Returns a `Rx` instace with [this] `T` as initial value.
Rx<T> get obs => Rx<T>(this);
}
小结:
适合界面上 简单状态管理,写起来很快
GetX
编写控制器 lib/pages/state_getx/controller.dart
class CountController extends GetxController {
final _count = 0.obs;
set count(value) => this._count.value = value;
get count => this._count.value;
final _count2 = 0.obs;
set count2(value) => this._count2.value = value;
get count2 => this._count2.value;
add() => _count.value++;
add2() => _count2.value++;
}
编写视图 lib/pages/state_getx/index.dart
class StateGetxView extends StatelessWidget {
StateGetxView({Key? key}) : super(key: key);
final controller = CountController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Getx"),
),
body: Center(
child: Column(
children: [
GetX<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetX - 1");
return Text('value 1 -> ${_.count}');
},
),
GetX<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetX - 2");
return Text('value 2 -> ${_.count}');
},
),
Divider(),
//
GetX<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetX - 3");
return Column(
children: [
Text('value 3 -> ${_.count}'),
ElevatedButton(
onPressed: () {
_.add();
},
child: Text('count1'),
)
],
);
},
),
Divider(),
// count2
GetX<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetX - 4");
return Text('value 4 -> ${_.count2}');
},
),
Divider(),
// 按钮
ElevatedButton(
onPressed: () {
controller.add();
},
child: Text('count1'),
),
ElevatedButton(
onPressed: () {
controller.add2();
},
child: Text('count2'),
),
],
),
),
);
}
}
小结:
适合控制多控制器、多状态更新,可精细控制初始、局部渲染。
GetBuilder
控制器 lib/pages/state_getBuilder/controller.dart 同上,不再重复
视图 lib/pages/state_getBuilder/index.dart
class StateGetBuilderView extends StatelessWidget {
StateGetBuilderView({Key? key}) : super(key: key);
final controller = CountController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("GetBuilder"),
),
body: Center(
child: Column(
children: [
GetBuilder<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetBuilder - 1");
return Text('value -> ${_.count}');
},
),
GetBuilder<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetBuilder - 2");
return Text('value -> ${_.count}');
},
),
Divider(),
//
GetBuilder<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetBuilder - 3");
return Column(
children: [
Text('value -> ${_.count}'),
ElevatedButton(
onPressed: () {
_.add();
},
child: Text('GetBuilder -> add'),
)
],
);
},
),
Divider(),
// count2
GetBuilder<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
print("GetBuilder - 4");
return Text('value count2 -> ${_.count2}');
},
),
Divider(),
// id2
GetBuilder<CountController>(
id: "id2",
init: controller,
initState: (_) {},
builder: (_) {
print("GetBuilder - 4");
return Text('id2 -> value count2 -> ${_.count2}');
},
),
Divider(),
// 按钮
ElevatedButton(
onPressed: () {
controller.add();
},
child: Text('add'),
),
ElevatedButton(
onPressed: () {
controller.add2();
},
child: Text('add2'),
),
ElevatedButton(
onPressed: () {
controller.update();
},
child: Text('controller.update()'),
),
ElevatedButton(
onPressed: () {
controller.update(["id2"]);
},
child: Text('controller.update(id2)'),
),
],
),
),
);
}
}
小结
和 GetX
比起来,多了手动控制更新,有两点需要注意。
controller.update();
触发更新
id: "id2",
标记哪个 builder
,触发方式 controller.update(["id2"]);
,可传多个 Array
类型。
ValueBuilder
lib/pages/state_valueBuilder/index.dart
class StateValueBuilderView extends StatelessWidget {
StateValueBuilderView({Key? key}) : super(key: key);
@overrideWidget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("ValueBuilder"),
),
body: Column(
children: [
Center(
child: ValueBuilder<int?>(
initialValue: 10,
builder: (value, updateFn) {
return Column(
children: [
Text("count -> " + value.toString()),
ElevatedButton(
onPressed: () {
updateFn(value! + 1);
},
child: Text('ValueBuilder -> add'),
)
],
);
},
// builder: (value, updateFn) => Switch(// value: value,// onChanged:// updateFn, // same signature! you could use ( newValue ) => updateFn( newValue )// ),// if you need to call something outside the builder method.
onUpdate: (value) => print("Value updated: $value"),
onDispose: () => print("Widget unmounted"),
),
),
],
),
);
}
}
小结:
适合局部的状态管理,很灵活。
防抖、限流
控制器 lib/pages/state_workers/controller.dart
class CountController extends GetxController {
final _count = 0.obs;
set count(value) => this._count.value = value;
get count => this._count.value;
add() => _count.value++;
@override
void onInit() {
super.onInit();
// 每次
ever(_count, (value) {
print("ever -> " + value.toString());
});
// 第一次
once(_count, (value) {
print("once -> " + value.toString());
});
// 防抖 2 秒内
debounce(
_count,
(value) {
print("debounce -> " + value.toString());
},
time: Duration(seconds: 2),
);
// 定时器 1 秒
interval(
_count,
(value) {
print("interval -> " + value.toString());
},
time: Duration(seconds: 1),
);
}
}
视图 lib/pages/state_workers/index.dart
class StateWorkersView extends StatelessWidget {
StateWorkersView({Key? key}) : super(key: key);
final controller = CountController();
@overrideWidget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("GetBuilder"),
),
body: Center(
child: Column(
children: [
// 显示GetX<CountController>(
init: controller,
initState: (_) {},
builder: (_) {
return Text('value -> ${_.count}');
},
),
// 按钮ElevatedButton(
onPressed: () {
controller.add();
},
child: Text('add'),
),
],
),
),
);
}
}
小结:
ever 适合做监听、日志收集
debounce 适合做搜索输入框