flutter插件get使用指南

1,525 阅读6分钟

介绍

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

安装

flutter pub add get

响应式开发

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",跳转到源码的位置。

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

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

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

RxString s = RxString("s");

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

点击 RxInt,直接查看源码。

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 相关的源码。

/// 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() 方法,则会抛出一个异常。

示例代码:

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

_RxImpl(T initial) {
    _value = initial;
}

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

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

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

/// 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

路由管理

路由跳转

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

结合起来就是下面这样:

if(data == 'success') doSomething();

命名路由导航

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

路由传参

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

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

接收参数

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

类似web使用querystring

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

接收参数

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

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

void main() {
  runApp(GetMaterialApp(
    initialRoute: '/',
    getPages: [
      GetPage(name: '/', page: () => const Home()),
      GetPage(
        name: '/other/:user', // 定义路由参数
        page: () => const Other(),
        transition: Transition.leftToRight, // 定义路由跳转动画
      ),
    ],
  ));
}

传递参数

Get.toNamed('/other/1')

接收参数

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

路由中间件

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

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

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

class MyMiddleWare extends GetMiddleware {
  @override
  RouteSettings? redirect(String? route) {
    print('route:$route');
    bool isLogin = false;
    return isLogin ? null : const RouteSettings(name: '/login');
  }
}

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

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

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

打开Dialog

Get.dialog(YourDialogWidget());

打开默认Dialog

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

打开BottomSheets

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 使用,我们就可以使用下面的代码获取到控制器。

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

集成管理

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

创建一个类并实现Binding

class Controller extends GetxController {
  var count = 0.obs;
  increment() => count++;
}

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

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

void main() => runApp(
      GetMaterialApp(initialRoute: '/', getPages: [
        GetPage(
          name: '/',
          page: () => const Home(),
          binding: CountBinding(),
        ),
        GetPage(
          name: '/details',
          page: () => Other(),
          binding: CountBinding(),
        ),
      ]),
    );

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

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

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

GetMaterialApp(
  initialBinding: SampleBind(),
  home: Home(),
);

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

// 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。

abstract class GetxController extends DisposableInterface
    with ListenableMixin, ListNotifierMixin

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

abstract class DisposableInterface extends GetLifeCycle

最后发现 GetLifeCycle 继承自 GetLifeCycleBase。

abstract class GetLifeCycle with GetLifeCycleBase

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

/// 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();
  }

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

class Controller extends GetxController {
  var count = 0.obs;

  increment() => count++;

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

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

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