对Flutter GetX的一些理解
GetX的优势
- 内部实现了路由管理,相比目前主流的fluro框架更轻量,并且路由跳转无需上下文对象,支持自定义路由中间件和动态路由传参等功能。
- 提供两种简单灵活的实现状态管理的方式。
- 它内部实现了依赖注入,可以快速的获取到某个状态管理器(GetxController)。
- 在实际开发中,通过上述三点配合使用,可以将界面、业务、路由、依赖等进行分离。在做到UI刷新及跨界面交互的同时,又能提高整体模块代码的可维护性和架构的可扩展性
- 提供修改全局语言/主题和其他的一些高级Api以及GetUtils工具类等。
- 可以全局获取上下文对象,所以上述场景都是不需要Context的。
- 只有用到的模块才会被编译到代码中,不会导致包体积增大。
- ....
状态管理
常用的几种状态管理对比
- Bloc非常安全和高效,但是模版代码太多,实现太过复杂。
- Provider内部使用InheritedWidget且依赖上下文对象,对其ChangeNotifier类的任何访问都必须在widget树或widget子树内。
- Fish_redux层次划分是比较细的,但是写起来很费劲,每次都要生成很多文件。
- 那其实每种方式都有其优缺点,但是Get并不是比任何其他状态管理更好,而是通过和它提供的其他模块搭配使用,使得模块代码更简单灵活且易维护而已。
GetxController *
-
主要是用于分离UI代码与业务逻辑
-
提供可以手动刷新UI的方法(update())
-
提供和StatefulWidget类似的生命周期,常用的有以下这三个方法
- onInit:数据初始化、加载缓存等处理
- onReady:界面渲染第一帧后调用,刷新UI的操作需要在这里处理
- onClose:做一些清除资源等处理
Get提供响应式状态 和简单状态两种状态管理器
响应式状态管理器
主要通过Obx和GetX Widget实现,但是GetX Widget 会多消耗内存,所以只介绍Obx的使用
-
创建控制器并继承GetxController,通过.obs扩展方法声明响应式变量(Rx)
class Controller extends GetxController { var count = 0.obs } extension DoubleExtension on double { RxDouble get obs => RxDouble(this); }
-
使用Obx方法实现定点刷新
final logic = Get.find<Controller>(); Obx(() => Text( '${logic.count.value}', ));
-
Obx()方法刷新的条件
- 只有当响应式变量的值发生变化时,才会执行刷新操作,当某个变量初始值为:0,再赋值为:0,并不会执行刷新操作
- 该响应式变量改变时,只有包裹该响应式变量的Obx()方法才会执行刷新操作(局部刷新)
简单状态管理器GetBuilder
-
创建控制器并继承GetxController
class Controller extends GetxController { int counter = 0; void increment() { counter++; update(); } }
-
通过GetBuilder包裹想要刷新的UI
// Stateless/Stateful GetBuilder<Controller>( // 未注入的控制器需要进行初始化 init: Controller(), builder: (_) => Text( '${_.counter}', ), )
-
当多处引用了同一个属性,但只想单独更新某一个地方,那么就可以用
UniqueID
来进行区分。class Controller extends GetxController { int counter = 0; void increment() { counter++; update(['hc_count']); } } GetBuilder<CountController>( builder: (controller) { return Text( "${controller.counter}", style: TextStyle(color: Colors.red, fontSize: 30), ); }, ), GetBuilder<CountController>( id: 'hc_count', builder: (controller) { return Text( "${controller.counter}", style: TextStyle(color: Colors.green, fontSize: 30), ); }, ),
但是一般这种场景很少见,不需要刷新的直接通过find Controller拿数据就可以。不需要用GetBuilder包裹。
或者说有应用其他场景,暂时没想到
总结
- Obx是配合响应式变量使用;GetBuilder是配合update使用。前者响应式变量变化,Obx自动刷新;后者需要使用update手动调用刷新。
- 每一个响应式变量,都会生成对应的
GetStream
,会对内存造成一定压力。 - 但GetBuilder内部实际上是对StatefulWidget的封装,所以占用资源极小。
- 一般来说,对于大多数场景都是可以使用响应式变量的。但是,在一个包含了大量对象的List,都使用响应式变量的话,将会生成大量的
GetStream
,会对内存造成较大的压力。该情况下,就要考虑使用简单状态管理了。 - 所以更推荐GetBuilder和update配合的方式,并且GetBuilder内置回收GetxController的功能。
依赖注入
依赖注入简单来说就是将一个类的实例注入另一个类,主要是为了将依赖组件的配置和使用分离开,降低使用者与依赖之间的耦合度。
Flutter中注入依赖项的基本方法是通过构造函数
class XXXWidget extends StatelessWidget {
Controller controller;
XXXWidget({this.controller});
}
class XXX1Widget extends StatelessWidget {
Controller1 controller;
XXX1Widget({this.controller});
}
class OtherPage extends StatelessWidget {
Controller someController = Controller();
Controller1 someController1 = Controller1();
@override
Widget build(BuildContext context) {
return Column(
children: [
XXXWidget(controller: someController),
XXX1Widget(controller: someController1),
],
);
}
}
可以看到这里还是存在一定的耦合。
那Get为了解决这种依赖的问题,提供了一个简单而强大的依赖管理器,只用1行代码就能检索到 Controller 或者需要依赖的类,不需要提供上下文,不需要在 inheritedWidget 的子节点
包含两种注入:立即注入和懒注入
内部实现:
- 将继承自GetxController的类的实例引用添加到内部的全局Map对象里
- 立即注入会在添加成功后主动调用find方法创建实例,可以直接使用
- 懒注入会在手动调用Get.find方法后,才可以使用
- 只有每次调用Get.find之后,才会真正创建实例。
立即注入
final logic = Get.put<LoginController>();
懒注入
Get.lazyPut(() => LoginController());
Get.find<LoginController>();
Get.lazyPut<S>(
// 必须:当你的类第一次被调用时,将被执行的方法。
InstanceBuilderCallback builder,
// 可选:和Get.put()一样,当你想让同一个类有多个不同的实例时,就会用到它。
// 必须是唯一的
String tag,
// 可选:下次使用时是否重建,
// 当不使用时,实例会被丢弃,但当再次需要使用时,Get会重新创建实例,
// 就像 bindings api 中的 "SmartManagement.keepFactory "一样。
// 默认值为false
bool fenix = false
)
当加载了无数条路由,现在需要拿到一个被遗留在某一个控制器中的数据,就不需要任何额外的依赖关系。只需要通过Get.find()就可以拿到对应的控制器数据,同样可以通过此方式实现跨界面交互。
Bindings
上面实现了依赖注入和使用,但是和前面讲的手动注入一样。都需要在 Widget 里注入,并没有完全解耦。那这个时候就可以通过Binding自动注入的方式去进行解耦
- 在路由跳转页面加载时注入当前页面所需的依赖关系。
- 可以将路由、状态管理器和依赖管理器完全集成。
- 当一个路由移除时,所有与它相关的控制器、变量和对象的实例都会从内存中移除。
- 同时Get可以通过它很清楚的知道当使用某个控制器时,哪个页面正在显示,并知道在哪里以及如何销毁它
使用
-
创建控制器以及继承自Bindings的类,重写dependencies方法进行懒注入控制器
class MainLogic extends GetxController {} class MainBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => MainLogic()); } }
-
使用该 Binding 来建立路由管理器、依赖关系和状态之间的连接。
使用命名路由
GetPage( name: RouteConfig.mainPage, page: () => const MainPage(), binding: MainBinding() // bindings: [MainBinding()], ),
-
使用
// 需刷新的UI,在界面无需手动注入以及指定init GetBuilder<HomeLogic>(builder: (logic) { // }), // 获取控制器数据 final logic = Get.find<MainLogic>(); logic.xxx
路由管理
分为普通路由和命名路由
上面提到的路由+状态管理+依赖注入配合使用,是需要通过命名路由的方式去实现配置。所以强烈推荐使用命名路由的方式,尤其是使用自动注入的时候。因此重点讲一下命名路由
配置
-
声明别名+注册路由表
class RouteConfig { static String mainPage = '/mainPage'; static final List<GetPage> getPages = [ GetPage( name: '/', page: () => UserLogic.hasAgreeProtocol() ? UserLogic.userInfo != null ? const MainPage() : LoginPage() : const ProtocolDialogPage(), bindings: [ if (UserLogic.userInfo != null) UserBinding(), MainBinding(), HomeBinding(), if (UserLogic.userInfo == null) LoginBinding(), GetCodeBinding() ]), GetPage( name: mainPage, page: () => const MainPage(), bindings: [UserBinding(), MainBinding(), HomeBinding()], transition: Transition.fadeIn), ]; }
-
替换MaterialApp为GetMaterialApp,指定getPages
GetMaterialApp( getPages: RouteConfig.getPages, // initialRoute: RouteConfig.xxx, // unknownRoute: RouteConfig.xxx, ));
-
如果没有配置根路由,还需要指定initialRoute
-
支持配置未知路由,避免跳转黑屏
跳转
无需Context。所以就可以在分离出的业务逻辑里执行跳转,而无需再维护一个和业务相关的UI界面的Context
- 跳转到下个界面
Get.toNamed(RouteConfig.mainPage);
- 跳转下一个界面并清除当前界面
Get.offNamed(RouteConfig.mainPage);
- 跳转下一个界面并清除之前所有界面
Get.offAllNamed(RouteConfig.mainPage);
- 返回上个界面
Get.back();
// 回传参数d
Get.back(result: T);
传递数据
只要发送你想要的参数即可。无论是一个字符串,一个Map,一个List,甚至一个类的实例。
Get.toNamed(RouteConfig.mainPage, arguments: 'Get is the best');
在你的类或控制器上:
Get.arguments
动态路由传参
Get.toNamed(RouteConfig.mainPage+'?id=xxx&name=xxx');
在你的类或控制器上:
Get.parameters['id']
使用场景Ex:H5携带参数跳转App界面、后端返回路由的地址进行跳转等
web端会在地址栏显示这个url
重复跳转
路由内部处理了多次点击重复跳转的问题,可以通过preventDuplicates解除此限制
Get.toNamed(RouteConfig.mainPage, preventDuplicates: false);
获取前一个路由
使用场景Ex:埋点前向地址(仅针对静态地址,动态地址还是要通过路由传值或者获取控制器数据进行处理)
String previousRoute = Get.previousRoute;
if (previousRoute.contains(RouteConfig.evalDetailPage)) {
referrer = '测评组详情';
} else if (previousRoute.contains(RouteConfig.myEvalPage)) {
referrer = '我的测评列表';
}
调用部分内置组件无需Context
无需Context即可调用Flutter提供的的Snackbar、Dialog、BottomSheet
-
Snackbar
Get.snackbar('Hi', 'i am a modern snackbar'); // 检查 snackbar 是否打开 Get.isSnackbarOpen
-
Dialog
Get.dialog(YourDialogWidget()); Get.defaultDialog( onConfirm: () => print("Ok"), middleText: "Dialog made in 3 lines of code" ); // 检查 dialog 是否打开 Get.isDialogOpen
-
BottomSheet
Get.bottomSheet( Container( child: // ) ); // 检查 bottomsheet 是否打开 Get.isBottomSheetOpen
-
同样是使用Get.back()关闭
路由GetPage中间件
它是在跳转某一个界面前做一些操作,比如:路由鉴权,判断登录状态、游客模式、青少年模式、部分界面权限处理等等
-
声明中间件
- 继承GetMiddleware
- 重写priority变量指定优先级,优先级越低越先执行
- 一般只需要重写redirect和onBindingsStart这两个方法;如果需要修改重定向路由的一些参数(跳转动画等),就需要重写onPageCalled方法
- redirect指定当前重定向的路由
- onBindingsStart:这个函数将在绑定初始化之前被调用, 可以更改此页面的绑定。(4.6.1版本上方法不执行)
import 'package:flutter/widgets.dart'; import 'package:happy_cece/common/logic/user/user_logic.dart'; import 'package:happy_cece/config/route/binding.dart'; import 'package:happy_cece/index.dart'; /// 登录状态中间件 class RouteLoginMiddleWare extends GetMiddleware { final int priorityT; RouteLoginMiddleWare({this.priorityT = 0}); @override int? get priority => priorityT; @override RouteSettings? redirect(String? route) { return ObjectUtil.isNotEmpty(UserLogic.userInfo?.apiToken) ? null : const RouteSettings(name: RouteConfig.loginPage); } @override List<Bindings>? onBindingsStart(List<Bindings>? bindings) { bindings ??= []; return bindings ..add(LoginBinding()) ..add(GetCodeBinding()); } }
onPageCalled
在调用页面时,这个函数会先被调用。 可以使用它来更改页面的某些内容或给它一个新页面。
应用场景:修改重定向的路由的一些配置:跳转动画,Bindings等。这种情况下redirect就不需要重写了,不然onPageCalled就会失效
import 'package:flutter/widgets.dart'; import 'package:happy_cece/common/logic/user/user_logic.dart'; import 'package:happy_cece/index.dart'; import 'package:happy_cece/page/login/view/login.dart'; import '../binding.dart'; /// 游客模式状态中间件 class RouteTouristMiddleWare extends GetMiddleware { final int priorityT; RouteTouristMiddleWare({this.priorityT = -1}); @override int? get priority => priorityT; // @override // RouteSettings? redirect(String? route) { // bool tourist = Constant.tourist; // if (tourist) { // showToast('当前是游客模式,请先登录'); // } // return !tourist ? null : const RouteSettings(name: RouteConfig.loginPage); // } @override GetPage? onPageCalled(GetPage? page) { if (Constant.tourist) { showToast('当前是游客模式,请先登录'); return GetPage( name: RouteConfig.loginPage, page: () => LoginPage(), bindings: [LoginBinding(), GetCodeBinding()], transition: Transition.downToUp); } return page; } }
其它方法基本用不到:
OnPageBuildStart
这个函数将在绑定初始化之后被调用。 在这里,您可以在创建绑定之后和创建页面widget之前执行一些操作。
GetPageBuilder onPageBuildStart(GetPageBuilder page) { print('bindings are ready'); return page; }
OnPageBuilt
这个函数将在GetPage.page调用后被调用,并给出函数的结果,并获取将要显示的widget。
OnPageDispose
这个函数将在处理完页面的所有相关对象(Controllers, views, ...)之后被调用。
-
在GetPage里注册
GetPage( name: RouteConfig.mainPage, page: () => const MainPage(), bindings: [UserBinding(), MainBinding(), HomeBinding()], middlewares: [RouteProtocolMiddleWare(), RouteLoginMiddleWare()], transition: Transition.fadeIn, ),
-
这个时候GetMaterialApp就可以指定mainPage为根路由,然后通过中间件的方式,就可以实现未同意协议去协议界面,未登录去登录界面的逻辑。相比上面提到的配置根路由的方式更优雅,且可以在多个界面绑定处理
GetMaterialApp( initialRoute: RouteConfig.mainPage, getPages: RoutePaths.getPages, )
分析如何使用GetX更好的组织项目架构和模块代码
通过命名路由的方式配置项目路由模块
- 配置路由名称
- 配置路由注册表
- 配置路由中间件和路由进行绑定
- 配置Bindings自动注入和路由进行绑定
- 替换为GetMaterialApp,并配置初始化路由
通过GetX插件快速生成模块代码
[[GetX代码生成插件] ] github.com/xdd666t/get…
一般会生成4个文件
- logic/controller:业务逻辑层
- state:状态层。在业务不复杂的情况下,可以直接放在业务逻辑层
- view:UI层
- Bindings:自动注入层,一般不会修改此文件,所以可以放到路由管理的那个Binding公共类
目的:将业务逻辑、状态、UI等进行解耦
配置全局状态管理器
如:通用验证码获取控制器、图片上传进度控制器、用户信息控制器、主程序控制器、全局语言/主题控制器等,并在适当的时候进行注入
业务模块配置
-
通过实际业务场景分析每个模块间的关联性,然后将通用,共用的业务逻辑代码或UI代码进行分离和封装到Controller逻辑层中。
-
模块间跨界面交互
- UI交互
- 业务逻辑交互
- 获取值
其他配置
- 公共组件
- 工具类
- 网络请求
- ...
转载自:https://juejin.cn/post/7112777461888352286