白筱汐

想都是问题,做都是答案

0%

flutter插件get使用指南

介绍

GetX,是一个 flutter 插件,它的 LIKES 非常非常多,目前为止是 LIKES 最多的 Flutter package。它功能很强大,而且使用便捷,支持路由管理,状态管理,响应式开发。有了它就可以直接一把梭哈,神速开发 Flutter 应用。 插件官方地址 get

安装

1
flutter pub add get

响应式开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import 'package:flutter/material.dart';
import 'package:get/get.dart'; // 引入依赖包

// 在 MaterialApp 前面加上 "Get"
void main() => runApp(const GetMaterialApp(home: Home()));

// 创建一个控制器
class Controller extends GetxController {
var count = 0.obs; // 在变量后面加上 'obs', 声明响应式变量

increment() => count++;
}

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

@override
Widget build(context) {
// 使用 Get.put() 对我们的控制器进行初始化,之后所有的子 widget 都可以访问到它了
final Controller c = Get.put(Controller());

return Scaffold(
// 使用 Obx() 方法返回一个 widget, 每当依赖的 c.count 发送变化,widget 都会重新新 build
appBar: AppBar(title: Obx(() => Text("Clicks: ${c.count}"))),

body: const Center(child: Text('get')),
floatingActionButton: FloatingActionButton(onPressed: c.increment, child: const Icon(Icons.add)));
}
}

现在,当我们点击添加按钮时,标题上的点击次数就会跟着变化,我们已经实现了响应式的开发。 太简单了!

第一眼看过去,”.obs”是什么东西。这其实是一个拓展语法,平时可能不太常见。简单的说就是在其他类上新增了一个方法。可以查看官网解释拓展方法

在编辑器里面点击 “.obs”,跳转到源码的位置。

1
2
3
4
extension IntExtension on int {
/// Returns a `RxInt` with [this] `int` as initial value.
RxInt get obs => RxInt(this);
}

翻译结果就是,返回一个 RxInt 类型的值,使用 .obs 前面的值作为初始值。

它其实是一个语法糖,类似的我们还可以使用 RxString,声明一个响应式的 String。

1
RxString s = RxString("s");

想要声明一个响应式的变量,我们直接在原有类型的变量后面加上 “.obs”,一把梭哈就完事了。

点击 RxInt,直接查看源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class RxInt extends Rx<int> {
RxInt(int initial) : super(initial);

/// Addition operator.
RxInt operator +(int other) {
value = value + other;
return this;
}

/// Subtraction operator.
RxInt operator -(int other) {
value = value - other;
return this;
}
}

翻译一下:operator 运算符重载,实现一个加法和减法操作,然后 RxInt 的 value 可以和 int 类型进行加减操作,然后返回 RxInt。

我们看到 RxInt 继承了 Rx, 然后我们看 Rx 相关的源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/// Foundation class used for custom `Types` outside the common native Dart
/// types.
/// For example, any custom "Model" class, like User().obs will use `Rx` as
/// wrapper.
class Rx<T> extends _RxImpl<T> {
Rx(T initial) : super(initial);

@override
dynamic toJson() {
try {
return (value as dynamic)?.toJson();
} on Exception catch (_) {
throw '$T has not method [toJson]';
}
}
}

Rx修饰了我们自定义的类,使用 Rx 包装之后它就是响应式的了。Rx 类还重写了父类的 toJson() 方法。在 toJson() 方法中,它尝试调用被包装对象的 toJson() 方法将其转换为 JSON 格式的数据。如果被包装对象没有定义 toJson() 方法,则会抛出一个异常。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import 'package:get/get.dart';

class Person {
String? name, last;
int age;

Person({this.name, this.last, required this.age});
}

class Controller extends GetxController {
final person = Person(name: 'John', last: 'Doe', age: 18).obs;

grow() {
person.update((val) {
val!.age++;
});
}
}

...

// 访问属性
Obx(() => Text('${c.person.value.age}'));

对于自定义的类,对类的实例使用 “.obs”,更新属性值时使用 update 方法,使用 value 属性去访问实例成员属性。

对于 _RxImpl 类,代码就比较复杂,它是响应式的核心。具体源码这里就不展示了,感兴趣的小伙伴可以自己去查看源码,这里只做一个简单的分析。

1
2
3
_RxImpl(T initial) {
_value = initial;
}

RxImpl 内部使用私有属性 _value 保存初始化的 value。这时候就有人要问了,你刚刚在说通过 value 属性去访问实例的成员属性的,这是怎么回事?

源码里面有一个抽象类 “abstract class _RxImpl extends RxNotifier with RxObjectMixin“。

注意这个 RxObjectMixin,它的内部有一段代码。

1
2
3
4
5
/// Returns the current [value]
T get value {
RxInterface.proxy?.addListener(subject);
return _value;
}

这里我们可以看到成员有一个 getter,当我们访问 value,实际上返回的是 _value,也就是存储的原始值。

如果我们需要设计一个响应式的系统,也就是说当数据源发送改变的时候,widget 会重新 build。是的,在前两篇文章我介绍了 Stream 和 StreamBuilder, 大体上就是通过 StreamController 添加数据,然后 listen 监听到数据流时会重新 build,从而更新 UI。

_RxImpl 内部有一个 Stream, 主要通过 StreamController 和 StreamSubscription 控制。当我们重新设置 value 的时候,内部会发送一个 Stream,然后监听者们就会收到通知做响应的处理。当然其内部逻辑具体实现还有很多细节,比如设置同样的值并不会触发更新,Stream 关闭的时候不能设置值等。

当我们访问属性值的时候,就会添加 GetStream 到监听者列表,方便查询监听者数量等。然后我们看 Obx(() => Text(‘${c.person.value.age}’)) 方法,它实际上返回的是一个 StatefulWidget, 内部的 build 方法被重写了,实际上调用的就是 Obx 传入的一个返回 widget 的回调函数。在 _ObxState 内部的 initState 生命周期监听 Stream,如果有数据流/事件通知,就调用 setState 重新触发 bulid,更新UI。

简化流程就是,首先设置响应式数据生成 Stream,改变响应式数据的时候,发送通知事件,Obx 内部监听Stream 重新 build,更新UI。具体内容请查看源码分析。

另外关于响应式的状态管理,还有 GetBuilder 可用,这里不在赘述。具体请查看 GetBuilder vs GetX vs Obx vs MixinBuilder

路由管理

路由跳转

  • 导航到新的页面
    1
    Get.to(NextScreen());
  • 关闭SnackBars、Dialogs、BottomSheets或任何你通常会用Navigator.pop(context)关闭的东西。
    1
    Get.back();
  • 替换当前页面,无法回到上一个页面
    1
    Get.off(NextScreen());
  • 删除所有路由记录,跳转到一个新的页面
    1
    Get.offAll(NextScreen());
  • 要导航到下一条路由,并在返回后立即接收或更新数据。
    1
    var data = await Get.to(Payment());
  • 返回上一个页面并传递数据
    1
    Get.back(result: 'success');
    结合起来就是下面这样:
    1
    if(data == 'success') doSomething();

命名路由导航

  • 导航到新的页面
    1
    Get.toNamed(NextScreen());
  • 替换当前页面,无法回到上一个页面
    1
    Get.offNamed(NextScreen());
  • 删除所有路由记录,跳转到一个新的页面
    1
    Get.offAllNamed(NextScreen());

路由传参

发送任意类型的数据,如一个Map

1
Get.toNamed('/second', arguments: {'id': 1});

接收参数

1
print(Get.arguments['id']);

类似web使用querystring

1
Get.toNamed('/second?id=2');

接收参数

1
print(Get.parameters['id']);

路由参数(需要定义路由)

1
2
3
4
5
6
7
8
9
10
11
12
13
void main() {
runApp(GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => const Home()),
GetPage(
name: '/other/:user', // 定义路由参数
page: () => const Other(),
transition: Transition.leftToRight, // 定义路由跳转动画
),
],
));
}

传递参数

1
Get.toNamed('/other/1')

接收参数

1
print(Get.parameters['user']); // 1

路由中间件

类似前端的vue开发,vue-router中路由的全局前置守卫和后置守卫在项目中经常被使用到。利用 Get 插件,我们可以实现类似的功能。

假如我们需要实现一个权限管理功能,没有登录的用户,点击某个页面需要跳转到登录页。

首先,定义一个路由中间件它继承于 GetMiddleware 并重写 redirect 方法,GetMiddleware有很多方法,读者可自行查看源码。

1
2
3
4
5
6
7
8
class MyMiddleWare extends GetMiddleware {
@override
RouteSettings? redirect(String? route) {
print('route:$route');
bool isLogin = false;
return isLogin ? null : const RouteSettings(name: '/login');
}
}

然后将它配置到路由表使用,我们也可以使用多个路由中间件,并可以设置中间件的权重,也就是控制多个中间价的执行顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void main() {
runApp(GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => const Home()),
GetPage(name: '/login', page: () => const Login()),
GetPage(
name: '/other/:user',
page: () => const Other(),
transition: Transition.leftToRight, // 定义路由跳转动画
middlewares: [MyMiddleWare()] // 使用路由中间件
),
],
routingCallback: (routing) {
print('current route:${routing?.current}');
}));
}

类似路由跳转后触发的后置守卫就是代码中的 routingCallback 方法,可以监听路由跳转的发生然后做相应的处理。

非跳转导航

在 flutter 中经常需要打开消息弹窗等操作,页面没有发送跳转,但是需要使用 Navigator.pop(context) 关闭。他们的使用通知都依赖于 context,现在使用 get 插件,我们可以很容易实现类似消息弹窗的功能(使用 Get.back() 关闭它们)。

打开 SnackBar

1
Get.snackbar('Hi', 'i am a modern snackbar');

打开Dialog

1
Get.dialog(YourDialogWidget());

打开默认Dialog

1
2
3
4
Get.defaultDialog(
onConfirm: () => print("Ok"),
middleText: "Dialog made in 3 lines of code"
);

打开BottomSheets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Get.bottomSheet(
Container(
child: Wrap(
children: <Widget>[
ListTile(
leading: Icon(Icons.music_note),
title: Text('Music'),
onTap: () {}
),
ListTile(
leading: Icon(Icons.videocam),
title: Text('Video'),
onTap: () {},
),
],
),
)
);

依赖管理

在前面的响应式状态管理章节,我们的状态使用 Get.put() 插入了依赖关系,并且返回了一个控制器,我们可以直接使用它。还有可能我们在父 widget 插入了依赖关系。在子 widget 使用,我们就可以使用下面的代码获取到控制器。

1
final controller = Get.find<Controller>(); // 通过范型获取

集成管理

将路由、状态管理器和依赖管理器完全集成,可以简化许多操作。

创建一个类并实现Binding

1
2
3
4
5
6
7
8
9
10
11
class Controller extends GetxController {
var count = 0.obs;
increment() => count++;
}

class CountBinding implements Bindings {
@override
void dependencies() {
Get.lazyPut<Controller>(() => Controller());
}
}

然后在你的命名路由定义的时候绑定它们即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void main() => runApp(
GetMaterialApp(initialRoute: '/', getPages: [
GetPage(
name: '/',
page: () => const Home(),
binding: CountBinding(),
),
GetPage(
name: '/details',
page: () => Other(),
binding: CountBinding(),
),
]),
);

现在所有的绑定的路由都可以获取并使用 Controller。

1
2
3
final Controller c = Get.find();
// 或者
final c = Get.find<Controller>();

你也可以通过 “initialBinding” 来插入所有将要创建的依赖。在 GetMaterialApp 内部,把所有控制器绑定到一起,然后实例化。

1
2
3
4
GetMaterialApp(
initialBinding: SampleBind(),
home: Home(),
);

通过 GetView 可以简化控制器的获取操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// GetView<T> 通过范型获取了对应的控制器
class Other extends GetView<Controller> {

Other({super.key});

@override
Widget build(context) {
return Scaffold(
appBar: AppBar(
title: const Text('other'),
),
// 访问控制器 'controller.xxx'
body: Center(child: Text("${controller.count}")));
}
}

生命周期

通过编辑器点击 GetxController,进入源码,我们看见 GetxController 是一个抽象类,它继承自 DisposableInterface。

1
2
abstract class GetxController extends DisposableInterface
with ListenableMixin, ListNotifierMixin

然后我们查看 DisposableInterface,它继承自 GetLifeCycle。

1
abstract class DisposableInterface extends GetLifeCycle

最后发现 GetLifeCycle 继承自 GetLifeCycleBase。

1
abstract class GetLifeCycle with GetLifeCycleBase

查看 GetLifeCycleBase 源码,发现它有很多方法。其中的生命周期作者都做了对应的解释,读者可以自己去尝试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
/// The [GetLifeCycle]
///
/// ```dart
/// class SomeController with GetLifeCycle {
/// SomeController() {
/// configureLifeCycle();
/// }
/// }
/// ```
mixin GetLifeCycleBase {
/// Called at the exact moment the widget is allocated in memory.
/// It uses an internal "callable" type, to avoid any @overrides in subclases.
/// This method should be internal and is required to define the
/// lifetime cycle of the subclass.
final onStart = InternalFinalCallback<void>();

// /// The `configureLifeCycle` works as a constructor for the [GetLifeCycle]
// ///
// /// This method must be invoked in the constructor of the implementation
// void configureLifeCycle() {
// if (_initialized) return;
// }

/// Internal callback that starts the cycle of this controller.
final onDelete = InternalFinalCallback<void>();

/// Called immediately after the widget is allocated in memory.
/// You might use this to initialize something for the controller.
void onInit() {}

/// Called 1 frame after onInit(). It is the perfect place to enter
/// navigation events, like snackbar, dialogs, or a new route, or
/// async request.
void onReady() {}

/// Called before [onDelete] method. [onClose] might be used to
/// dispose resources used by the controller. Like closing events,
/// or streams before the controller is destroyed.
/// Or dispose objects that can potentially create some memory leaks,
/// like TextEditingControllers, AnimationControllers.
/// Might be useful as well to persist some data on disk.
void onClose() {}

bool _initialized = false;

/// Checks whether the controller has already been initialized.
bool get initialized => _initialized;

// Internal callback that starts the cycle of this controller.
void _onStart() {
if (_initialized) return;
onInit();
_initialized = true;
}

bool _isClosed = false;

/// Checks whether the controller has already been closed.
bool get isClosed => _isClosed;

// Internal callback that starts the cycle of this controller.
void _onDelete() {
if (_isClosed) return;
_isClosed = true;
onClose();
}

以下给出一个测试生命周期的案例。

1
2
3
4
5
6
7
8
9
10
11
12
class Controller extends GetxController {
var count = 0.obs;

increment() => count++;

@override
void onReady() {
super.onReady();
print('onReady');
// 发起网络请求
}
}

在 onReady 生命周期的时候,我们打印了 ‘onReady’,可以在这时候发送网络请求。

建议查看 github包管理地址,通过编辑器的点击跳转,阅读对应的源码,能更好的帮助你了解它的内部机制。