likes
comments
collection
share

Flutter开发 -- 使用GetX实现状态管理

作者站长头像
站长
· 阅读数 30

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方法里,修改MaterialAppGetMaterialApp

添加第一个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
  }
}
  1. 成员变量,存放todo列表。
  2. getter。
  3. setter。
  4. 发出变更消息。通知所有“消费”这个变更的Widget读取新的值,并绘制在界面上。

看到update,是否发现和其他的几个状态管理工具多少有些类似。当前的值发生变化之后通知“消费者”发生了变更。和notifyListenersinvalidateSelf这些方法的使用很像。的确如此。

当然,如果你已经看过了GetX的文档,你会发现也可以这样定义这个todo列表:

RxList<TodoItem> _todoList = [].obs;

这个是GetX的特色之一,后面在实现排序的功能,以及其他的功能的时候会使用这个方式。使用obsupdate之间的区别,目前只是牵着没有显示的调用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
  }
  1. 使用了GetX的依赖注入,这里直接获取TodoRepository实例,调用它的方法获取todo列表。
  2. 给setter赋值,间接调用了update方法。

服务端返回了todo列表之后就该在UI上显示这些数据了。在UI显示这些状态的变更有三个选择,这里我们使用GetBuilder。其他的几个:ObxGetX会在后面陆续介绍。

新建一个页面:HomeScreen。在这里显示从服务端返回的todo列表。一个StatelessWidget就足够满足我们的需要了:

class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    // 略
  }
}

之后在build里返回一个带有ListView的widget。这个Widget需要响应HomeControllertodoList的变化。所以在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: // 略
          )
          // 略
        ),
      )
    );
  }
}
  1. GetBuilder<HomeController>,在使用这个builder的时候有一个类型参数,需要是你的Controller类型。
  2. 初始化你的controller。·Get.find()`,是使用了GetX的依赖注入部分。后面会详细讲到。涉及到简单的配置,所以这里可以把这个Get.find部分当做一个黑盒。
  3. 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这个方法外,还有disposebuilder。builder这个就��用说了,把数据绘制到界面上。dispose主要用来在widget被清理的同时,清理掉相关的资源。比如,在上面代码中关闭了

我们要说的是GetxController的声明周期。获取数据的api也可以在controller的构造函数来实现。但是,这严重违反了一个以性能著称的第三方库的开发原则。所以:onInitonClose两个方法应运而生。

所以GetBuilderinitState也可以放在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>()

我们在一个例子里俩看看这个如何实现。

用户在设置页面,选择排序,在弹出的菜单选择排序的选项:过期时间、创建时间。选择之后,在首页也就是列表页可以看到排序的两个选项。默认是降序排列。

选择排序选项:

Flutter开发 -- 使用GetX实现状态管理**

排序选项在首页的显示:

Flutter开发 -- 使用GetX实现状态管理**

从状态管理的角度来说,变更发生在设置页的排序选项上,在界面上除了要在排序选项上有选中的效果,还要再首页显示排序的选项。现在用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;
  }

  // 其他略
}
  1. 用来保存排序的字段和顺序
  2. 定义obs,让排序列表变成观察者。注意,这一段GetX的文档没改,已经不能用了。按照本文的方法才能初始化。
  3. 直接返回观察者列表。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。如图:

Flutter开发 -- 使用GetX实现状态管理**

  1. 排序选项本身需要响应用户选择的变化,没有选中的效果和选中之后的效果是不一样的。所以需要Obx接收状态的变更。
  2. ListTileonTap事件中,来处理选中和取消选中。

每次点击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里,如果这个字段存在那么删除它。在执行了addremoveWhere之后状态变更的消费者会受到这个变更的通知。为什么呢,我们看看他们实现的代码:

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();
  }
}
  1. 添加的实现,会调用refresh方法。
  2. 删除符合给定条件的元素,会调用refresh方法。

这个refresh方法就是用来把变更通知发送出去。

那么,来看看首页如何处理这些变更。看代码:

  final homeController = Get.find<HomeController>();  // 1

  // 略去部分代码
  Obx(() {  // 2
    if (homeController.sortings.isNotEmpty) {
      return getSortControl();
    }
    return const SizedBox(   // 3
      width: 1,
      height: 1,
    );
  }),
  1. builder方法里获取home controller。
  2. 使用Obx消费变更,显示或者隐藏首页的排序控件。
  3. 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'));
  1. 首先获取到这个排序对象。
  2. 如果这个对象不为空,那么更新这个对象。这个时候需要更改排序的顺序了。这里使用了把观察者对象当做方法使用的方法来更新排序顺序。

修改显示的箭头顺序

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的依赖注入了。

  1. 继承GetX提供的Bindings
  2. dependencies里调用put或者lazyPut把初始化你的Controller或者其他的需要依赖注入的方法放进去。

最后

通过上面的例子,你已经熟悉了使用update和使用GetX专属的obs的方式实现状态管理的方式。

同时,你也可以使用GetBuilderObx来消费状态的变更了。这两个看起来是不是非常简单。

GetX还提供了很多其他的功能,比如路由导航、本地化和一些常用的控件等等。总之使用GetX了以后头也不疼了,眼也不花了。简直是居家旅行,写代码搬砖的必备佳品。

转载自:https://juejin.cn/post/7361752279717953588
评论
请登录