Flutter开发 -- 使用GetX实现状态管理
GetX不仅仅是一个状态管理工具。在GetX包下,可以使用的有包含状态管理在内的导航、本地化和依赖注入等,甚至还包括一小部分的几个Widget。它的状态管理非常简单高效,最关键不需要写很多的“样板”代码。本文将详细介绍GetX的用法,基础的和高级的都包括在内。帮助你深入了解这个工具,达到活学活用的目的。
代码在这里。跟着代码,看本文效果更加!
简介
什么是GetX,和StatefulWidget、Provider、Riverpod这些类似,它是Flutter的一个轻量级的却功能丰富的状态管理库。
我们反复强调GetX简单,那么都简单在哪里了呢?使用GetX不用写其他库要求的那些“样板”代码,也不需要什么七七八八的配置才能用的代码生成工具。而且GetX性能非常好,但是内存占用的还少。GetX还附带了一个依赖注入工具。和状态管理一起使用效果更佳!
安装依赖
打开我们的老朋友todo app。如果你是新建的一个项目的话开始安装依赖:
修改pubspec.yaml文件
dependencies:
get:
然后执行命令:flutter pub get
完成安装。
修改MaterialApp
打开main.dart文件。
在正式开始使用前记得引入依赖:
import 'package:get/get.dart';
在main方法里,修改MaterialApp
为GetMaterialApp
。
添加第一个Controller
GetX的主要目的之一就是要分离界面和逻辑,这就需要用到GetXController
。
当然,这样的controller的数量是不限的。它们就是用来负责处理App的状态的。这样与这些controller相对应的观察者们,就是类似于Provider里的Consumer
,在状态发生变化之后重新绘制界面。这里,我们添加一个HomeController
的controller,他主要用来获取todo的列表。其他的controller后续陆续添加。
这个HomeController
看起来是这样的:
class HomeController extends GetxController {
// 略
}
在这个controller里,包含处理todo列表和这个todo列表排序的问题。这个controller主要用在HomeScreen
里。
实现会到HomeController
。首先需要一个存放todo列表的地方:
class HomeController extends GetxController {
List<TodoItem> _todoList = []; // 1
get todoList => _todoList; // 2
set todoList(value) { // 3
_todoList = value;
update(); // 4
}
}
- 成员变量,存放todo列表。
- getter。
- setter。
- 发出变更消息。通知所有“消费”这个变更的Widget读取新的值,并绘制在界面上。
看到update
,是否发现和其他的几个状态管理工具多少有些类似。当前的值发生变化之后通知“消费者”发生了变更。和notifyListeners
,invalidateSelf
这些方法的使用很像。的确如此。
当然,如果你已经看过了GetX的文档,你会发现也可以这样定义这个todo列表:
RxList<TodoItem> _todoList = [].obs;
这个是GetX的特色之一,后面在实现排序的功能,以及其他的功能的时候会使用这个方式。使用obs
和update
之间的区别,目前只是牵着没有显示的调用update
而已。
接下来,我们需要一个方法来从服务端获取todo列表:
Future<void> fetchTodoList(
{bool all = true, String completed = 'completed'}) async {
final todoList = await Get.find<TodoRepository>() // 1
.getAllTodoList(completed: 'uncompleted');
this.todoList = todoList ?? []; // 2
}
- 使用了GetX的依赖注入,这里直接获取
TodoRepository
实例,调用它的方法获取todo列表。 - 给setter赋值,间接调用了
update
方法。
服务端返回了todo列表之后就该在UI上显示这些数据了。在UI显示这些状态的变更有三个选择,这里我们使用GetBuilder
。其他的几个:Obx
和GetX
会在后面陆续介绍。
新建一个页面:HomeScreen
。在这里显示从服务端返回的todo列表。一个StatelessWidget
就足够满足我们的需要了:
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
// 略
}
}
之后在build
里返回一个带有ListView的widget。这个Widget需要响应HomeController
的todoList
的变化。所以在ListView外面用到之前提到的GetX的GetBuilder
。
这些大概看起来是这样的:
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(appBar: // 略
body: GetBuilder<HomeController>( // 1
init: Get.find<HomeController>(), // 2
builder: (controller) => ListView.separated(
itemCount: controller.todoList.length, // 3
itemBuilder: (context, index) => ListTile(
title: // 略
)
// 略
),
)
);
}
}
GetBuilder<HomeController>
,在使用这个builder的时候有一个类型参数,需要是你的Controller
类型。- 初始化你的controller。·Get.find()`,是使用了GetX的依赖注入部分。后面会详细讲到。涉及到简单的配置,所以这里可以把这个Get.find部分当做一个黑盒。
- 在
builder
方法里直接使用参数里的controller
来获得todo列表,并绘制在ListTile
里。
似乎大功告成了呢。这个时候如果你运行代码的话,界面还是白板。因为,获取todo列表的方法fetchTodoList
方法还没有调用呢,这不会有todo列表返回的,即使服务端有数据也什么都不会显示。
GetxController的生命周期
GetxController
有声明周期方法,GetBuilder
也有。比如我们在上文的问题,可以在GetBuilder
的生命周期方法initState
里实现。看代码:
GetBuilder<Controller>(
initState: (_) => Controller.to.fetchApi(),
dispose: (_) => Controller.to.closeStreams(),
builder: (s) => Text('${s.username}'),
),
除了initState
这个方法外,还有dispose
和builder
。builder这个就��用说了,把数据绘制到界面上。dispose
主要用来在widget被清理的同时,清理掉相关的资源。比如,在上面代码中关闭了流。
我们要说的是GetxController的声明周期。获取数据的api也可以在controller的构造函数来实现。但是,这严重违反了一个以性能著称的第三方库的开发原则。所以:onInit
和onClose
两个方法应运而生。
所以GetBuilder
的initState
也可以放在controller的onInit
方法里面来调用。如:
class YourController extends GetxController {
@override
void onInit() {
fetchApi();
super.onInit();
}
}
那么类似的,如果有需要释放资源,比如关闭流的时候,就可以使用controller的onClose()
方法了。
使用obs
在更新todo列表的时候,我们使用了update
方法。在描述中也提到,还有其他的方法可以发出变更,那就是使用obs。
如果你已经使用过或者了解Provider
类的模式的话,使用update的会让你更加容易理解GetX的方式。但是使用obs才是更加简便、直接的方式。也是GetX状态管理的精髓。
var name = 'Stephen Curry';
这是一个普通的变量,它的值是一个字符串。那么怎么变成GetX使用的观察者呢,看代码:
var name = 'Stephen Curry'.obs;
就是这么简单。
除了字符串类型的obs变化之外,其他的类型也可以,比如:
那么如何消费这个观察者发出的变更呢,也很简单:
Obx (() => Text (controller.name));
非常的简单,如果你问那么这个controller
哪里来的。还是用GetX内置的依赖注入:final controller = Get.find<YourController>()
。
我们在一个例子里俩看看这个如何实现。
用户在设置页面,选择排序,在弹出的菜单选择排序的选项:过期时间、创建时间。选择之后,在首页也就是列表页可以看到排序的两个选项。默认是降序排列。
选择排序选项:
**
排序选项在首页的显示:
**
从状态管理的角度来说,变更发生在设置页的排序选项上,在界面上除了要在排序选项上有选中的效果,还要再首页显示排序的选项。现在用GetX的obs来实现这个功能。
在HomeController
里增加一个观察者属性,因为有两个,一个due date一个creation date,同时为了更好的显示GetX的用法我们用列表来实现一个Obs。看代码:
class SortType { // 1
String sortField = '';
String order = '';
SortType({required this.sortField, required this.order});
}
class HomeController extends GetxController {
RxList<SortType> _sortings = <SortType>[].obs; // 2
RxList<SortType> get sortings => _sortings; // 3
set sortings(List<SortType> value) {
_sortings = value.obs;
}
// 其他略
}
- 用来保存排序的字段和顺序
- 定义obs,让排序列表变成观察者。注意,这一段GetX的文档没改,已经不能用了。按照本文的方法才能初始化。
- 直接返回观察者列表。
List
的方法里,常用的几个可以直接在列表观察者上使用。
代码就是这些改动。现在看看在设置页面需要做些什么:
Obx(() => ListTile( // 1
title: const Text("Creation Date"),
selected: controller
.hasSuchField('creation date'),
trailing: controller
.hasSuchField('creation date')
? const Icon(
Icons.check,
color: Colors.blue,
)
: null,
onTap: () {
controller.toggleSorting(SortType( // 2
sortField: 'creation date',
order: 'desc'));
},
)
)
这段代码是选择排序选项的一个:creation date。如图:
**
- 排序选项本身需要响应用户选择的变化,没有选中的效果和选中之后的效果是不一样的。所以需要
Obx
接收状态的变更。 - 在
ListTile
的onTap
事件中,来处理选中和取消选中。
每次点击ListTile
选中或者取消排序选项的时候会调用HomeController.toggleSorting
方法。代码如下:
void toggleSorting(SortType sortType) {
final result = _sortings.where((e) => e.sortField == sortType.sortField);
if (result.isEmpty) {
_sortings.add(sortType);
return;
}
_sortings.removeWhere((e) => e.sortField == sortType.sortField);
}
在这个方法里执行了两个事情,如果这个排序字段不存在,那么添加到sortings
里,如果这个字段存在那么删除它。在执行了add
和removeWhere
之后状态变更的消费者会受到这个变更的通知。为什么呢,我们看看他们实现的代码:
class RxList<E> {
@override
void add(E element) { // 1
_value.add(element);
refresh();
}
@override
void removeWhere(bool Function(E element) test) { // 2
_value.removeWhere(test);
refresh();
}
}
- 添加的实现,会调用
refresh
方法。 - 删除符合给定条件的元素,会调用
refresh
方法。
这个refresh
方法就是用来把变更通知发送出去。
那么,来看看首页如何处理这些变更。看代码:
final homeController = Get.find<HomeController>(); // 1
// 略去部分代码
Obx(() { // 2
if (homeController.sortings.isNotEmpty) {
return getSortControl();
}
return const SizedBox( // 3
width: 1,
height: 1,
);
}),
- 在
builder
方法里获取home controller。 - 使用
Obx
消费变更,显示或者隐藏首页的排序控件。 - Obx这玩意儿必须返回一个Widget,返回null不行。就用这个代替一下。
有没有发现,到目前为止还没有用到过StatefulWidget
,都是StatelessWidget
一路走到底。
修改排序顺序
在首页显示了排序的字段之后,那么根据字段修改排序的顺序也是很有必要的一个功能。默认的都是倒序排序,现在需要可以修改这个顺序。点击之后为正序,再点击之后倒序。这个时候有需要如何操作呢?
那肯定是要在之前定义的SortType
上做文章了。它的order
字段就是用来干这个的。默认是倒序desc
。点击之后变成是asc
。通过上面的讲解,各位应该知道直接修改SortType
的实例是没有什么作用的。
首先就要给她变成观察者。一个对象,如何变成观察者:
final sortType = SortType().obs;
直接在初始化的时候在后面追加obs
就可以了。
那么要怎么更新这个观察者呢:
final sortType = SortType(sortField: 'due date', order: 'desc').obs;
// 更新
sortType.update((s) => {
s.fieldName = 'new field';
s.order = 'new order';
});
// 或者
sortType(SortType(sortField: 'new field', order: 'new order'));
要更新一个SortType对象可以使用update
方法,或者把当前的这个观察者当做一个方法来使用,把一个新的SortType对象传进去就可以。简单!!!
要读取这个观察者对象的值如何操作呢,看代码:
final sortType = SortType(sortField: 'due date', order: 'desc').obs;
print('${sortType.value.sortField}');
// 或者
print('${sortType().sortField}');
在用户点击了排序按钮之后,修改排序的顺序之后那个按钮边上的箭头方向也对应个改变。这个如何操作呢,看代码:
在用户点击排序按钮之后,处理点击事件:
final sortType = _.getSortObserver('creation date'); // 1
if (sortType == null) return;
sortType(SortType( // 2
sortField: sortType.value.sortField,
order: sortType.value.order == 'desc'
? 'asc'
: 'desc'));
- 首先获取到这个排序对象。
- 如果这个对象不为空,那么更新这个对象。这个时候需要更改排序的顺序了。这里使用了把观察者对象当做方法使用的方法来更新排序顺序。
修改显示的箭头顺序
Obx(() => // 1
_.getSortObserver('creation date')?.value.order ==
'desc'
? const Icon(Icons.arrow_drop_down)
: const Icon(Icons.arrow_drop_up))
使用条件绘制,根据order
字段的值显示不同的箭头图片。
不得不说的依赖注入
这里简单介绍一下GetX的依赖注入。
在我们的代码中,每次用到一个controller的时候,都是用Get.find<YourController>()
这个方法来获得controller实例的。如果没有依赖注入,那么在用到controller的时候只能在哪里用在哪里final controller = YourController();
这样初始化一个。使用依赖注入方便了很多。那么,既然可以拿,那就需要有一个地方把这个实例放进去,或者至少要告诉GetX怎么新建一个。这就是GetX的依赖注入在干的事情。
GetX的Bindings
在main.dart里面配置了Bindings
。
class GetxBindings extends Bindings { // 1
@override
void dependencies() {
Get.put<TodoRepository>(TodoRepository(), permanent: true); // 2
Get.lazyPut<HomeController>(() => HomeController(), fenix: true); // 2
Get.lazyPut<DetailController>(() => DetailController()); // 2
}
}
在本文开始的地方就提到在*main.dart文件修改一些配置。这次又要修改一点。在
GetMaterialApp里增加
initialBinding`里初始化刚刚的bindings类:
GetMaterialApp(
initialBinding: GetxBindings(), // *
home: const HomePage(),
);
这样就可以开始使用GetX的依赖注入了。
- 继承GetX提供的
Bindings
。 - 在
dependencies
里调用put
或者lazyPut
把初始化你的Controller或者其他的需要依赖注入的方法放进去。
最后
通过上面的例子,你已经熟悉了使用update
和使用GetX专属的obs
的方式实现状态管理的方式。
同时,你也可以使用GetBuilder
和Obx
来消费状态的变更了。这两个看起来是不是非常简单。
GetX还提供了很多其他的功能,比如路由导航、本地化和一些常用的控件等等。总之使用GetX了以后头也不疼了,眼也不花了。简直是居家旅行,写代码搬砖的必备佳品。
转载自:https://juejin.cn/post/7361752279717953588