介绍
GetX,是一个 flutter 插件,它的 LIKES 非常非常多,目前为止是 LIKES 最多的 Flutter package。它功能很强大,而且使用便捷,支持路由管理,状态管理,响应式开发。有了它就可以直接一把梭哈,神速开发 Flutter 应用。 插件官方地址 get。
安装
shell
flutter pub add get
响应式开发
dart
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)));
}
}
现在,当我们点击添加按钮时,标题上的点击次数就会跟着变化,我们已经实现了响应式的开发。 太简单了!
第一眼看过去,".bos"是什么东西。这其实是一个拓展语法,平时可能不太常见。简单的说就是在其他类上新增了一个方法。可以查看官网解释拓展方法。
在编辑器里面点击 ".bos",跳转到源码的位置。
dart
extension IntExtension on int {
/// Returns a `RxInt` with [this] `int` as initial value.
RxInt get obs => RxInt(this);
}
翻译结果就是,返回一个 RxInt 类型的值,使用 .bos 前面的值作为初始值。
它其实是一个语法糖,类似的我们还可以使用 RxString,声明一个响应式的 String。
dart
RxString s = RxString("s");
想要声明一个响应式的变量,我们直接在原有类型的变量后面加上 ".bos",一把梭哈就完事了。
点击 RxInt,直接查看源码。
dart
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 相关的源码。
dart
/// 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() 方法,则会抛出一个异常。
示例代码:
dart
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}'));
对于自定义的类,对类的实例使用 ".bos",更新属性值时使用 update 方法,使用 value 属性去访问实例成员属性。
对于 _RxImpl 类,代码就比较复杂,它是响应式的核心。具体源码这里就不展示了,感兴趣的小伙伴可以自己去查看源码,这里只做一个简单的分析。
dart
_RxImpl(T initial) {
_value = initial;
}
RxImpl 内部使用私有属性 _value 保存初始化的 value。这时候就有人要问了,你刚刚在说通过 value 属性去访问实例的成员属性的,这是怎么回事?
源码里面有一个抽象类 "abstract class _RxImpl extends RxNotifier with RxObjectMixin"。
注意这个 RxObjectMixin,它的内部有一段代码。
dart
/// 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。
路由管理
路由跳转
- 导航到新的页面
dart
Get.to(NextScreen());
- 关闭SnackBars、Dialogs、BottomSheets或任何你通常会用Navigator.pop(context)关闭的东西。
dart
Get.back();
- 替换当前页面,无法回到上一个页面
dart
Get.off(NextScreen());
- 删除所有路由记录,跳转到一个新的页面
dart
Get.offAll(NextScreen());
- 要导航到下一条路由,并在返回后立即接收或更新数据。
dart
var data = await Get.to(Payment());
- 返回上一个页面并传递数据
dart
Get.back(result: 'success');
结合起来就是下面这样:
dart
if(data == 'success') doSomething();
命名路由导航
- 导航到新的页面
dart
Get.toNamed(NextScreen());
- 替换当前页面,无法回到上一个页面
dart
Get.offNamed(NextScreen());
- 删除所有路由记录,跳转到一个新的页面
dart
Get.offAllNamed(NextScreen());
路由传参
发送任意类型的数据,如一个Map
dart
Get.toNamed('/second', arguments: {'id': 1});
接收参数
dart
print(Get.arguments['id']);
类似web使用querystring
dart
Get.toNamed('/second?id=2');
接收参数
dart
print(Get.parameters['id']);
路由参数(需要定义路由)
dart
void main() {
runApp(GetMaterialApp(
initialRoute: '/',
getPages: [
GetPage(name: '/', page: () => const Home()),
GetPage(
name: '/other/:user', // 定义路由参数
page: () => const Other(),
transition: Transition.leftToRight, // 定义路由跳转动画
),
],
));
}
传递参数
dart
Get.toNamed('/other/1')
接收参数
dart
print(Get.parameters['user']); // 1
路由中间件
类似前端的vue开发,vue-router中路由的全局前置守卫和后置守卫在项目中经常被使用到。利用 Get 插件,我们可以实现类似的功能。
假如我们需要实现一个权限管理功能,没有登录的用户,点击某个页面需要跳转到登录页。
首先,定义一个路由中间件它继承于 GetMiddleware 并重写 redirect 方法,GetMiddleware有很多方法,读者可自行查看源码。
dart
class MyMiddleWare extends GetMiddleware {
@override
RouteSettings? redirect(String? route) {
print('route:$route');
bool isLogin = false;
return isLogin ? null : const RouteSettings(name: '/login');
}
}
然后将它配置到路由表使用,我们也可以使用多个路由中间件,并可以设置中间件的权重,也就是控制多个中间价的执行顺序。
dart
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
dart
Get.snackbar('Hi', 'i am a modern snackbar');
打开Dialog
dart
Get.dialog(YourDialogWidget());
打开默认Dialog
dart
Get.defaultDialog(
onConfirm: () => print("Ok"),
middleText: "Dialog made in 3 lines of code"
);
打开BottomSheets
dart
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 使用,我们就可以使用下面的代码获取到控制器。
dart
final controller = Get.find<Controller>(); // 通过范型获取
集成管理
将路由、状态管理器和依赖管理器完全集成,可以简化许多操作。
创建一个类并实现Binding
dart
class Controller extends GetxController {
var count = 0.obs;
increment() => count++;
}
class CountBinding implements Bindings {
@override
void dependencies() {
Get.lazyPut<Controller>(() => Controller());
}
}
然后在你的命名路由定义的时候绑定它们即可。
dart
void main() => runApp(
GetMaterialApp(initialRoute: '/', getPages: [
GetPage(
name: '/',
page: () => const Home(),
binding: CountBinding(),
),
GetPage(
name: '/details',
page: () => Other(),
binding: CountBinding(),
),
]),
);
现在所有的绑定的路由都可以获取并使用 Controller。
dart
final Controller c = Get.find();
// 或者
final c = Get.find<Controller>();
你也可以通过 "initialBinding" 来插入所有将要创建的依赖。在 GetMaterialApp 内部,把所有控制器绑定到一起,然后实例化。
dart
GetMaterialApp(
initialBinding: SampleBind(),
home: Home(),
);
通过 GetView 可以控制器的注册操作。
dart
// 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}")));
}
}
如果你想了解更多内容,建议查看 github 和 包管理地址,通过编辑器的点击跳转,阅读对应的源码,能更好的帮助你了解它的内部机制。