前言
使用 Bloc 的时候,有一个让我至今为止十分在意的问题,无法真正的跨页面交互!在反复的查阅官方文档后,使用一个全局 Bloc 的方式,实现了“伪”跨页面交互,详细可查看:flutter_bloc 使用解析 ;fish_redux 的广播机制是可以比较完美的实现跨页面交互的,我也写了一篇几万字文章介绍如何使用该框架:fish_redux 使用详解 ,redux 层次划分是比较细的,写起来会很费劲;最近尝试了 GetX 相关功能,解决了我的相当一部分痛点
把整篇文章写完后,我马上把自己的一个 demo 里面所有 Bloc 代码全用 GetX 替换,且去掉了 Fluro 框架;感觉用 Getx 虽然会省掉大量的模板代码,但还是有些重复工作:创建文件夹,创建几个必备文件,写那些必须要写的初始化代码和类;略微繁琐,为了对得起 GetX 给我开发带来的巨大便利,我就花了一些时间,给它写了一个插件! 上面这重复的代码,文件,文件夹统统能一键生成!
GetX 相关优势
依赖注入
GetX 是通过依赖注入的方式,存储相应的 XxxGetxController;已经脱离了 InheritedWidget 那一套玩法,自己手动去管理这些实例,使用场景被大大拓展
简单的思路,却能产生深远的影响:优雅的跨页面功能便是基于这种设计而实现的、获取实例无需 BuildContext、GetBuilder 自动化的处理及其减少了入参等等
跨页面交互
这绝对是 GetX 的一个优点!对于复杂的生产环境,跨页面交互的场景,实在太常见了,GetX 的跨页面交互,实现的也较为优雅
路由管理
getx 内部实现了路由管理,而且用起来,非常简单!bloc 没实现路由管理,我不得不找一个 star 量高的路由框架,就选择了 fluro,但是不得不吐槽下,fluro 用起来真的很折磨人,每次新建一个页面,最让我抗拒的就是去写 fluro 路由代码,横跨几个文件来回写,头皮发麻
GetX 实现了动态路由传参,也就是说直接在命名路由上拼参数,然后能拿到这些拼在路由上的参数,也就是说用 flutter 写 H5,直接能通过 Url 传值,OMG!可以无脑舍弃复杂的 fluro 了
实现了全局 BuildContext
国际化,主题实现
如果深度使用过 Provider,Bloc 这类依赖 InheritedWidget 建立起的状态管理框架;再看看 GetX 内部实现思想,就能发现,他们已经是俩种体系的东西了
对此,我来抛出一些问题:InheritedWidget 存在什么缺点?为什么其数据传递和路由设计思想对立?为什么 getx 使用依赖注入?getx 的 obx 自动刷新黑魔法是个什么鬼?
下来将全面的介绍 GetX 的使用,文章也不分篇水阅读量了,力求一文写清楚,方便大家随时查阅
准备 引入
1 2 3 4 5 6 7 # getx 状态管理框架 https: # 非空安全最后一个版本(flutter 2.0 之前版本) get : ^3.26 .0 # 空安全版本 最新版本请查看 https: get : ^4.3 .8
GetX 地址
主入口配置
只需要将MaterialApp
改成GetMaterialApp
即可
1 2 3 4 5 6 7 8 9 10 11 12 13 void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return GetMaterialApp( home: CounterGetPage(), ); } }
1 2 import 'package:get/get.dart' ;
插件 这个 getx 代码生成插件,我花了不少精力去完善,功能已经比较齐全了,希望对大家有所帮助。
欢迎大家提 issue,提 issue 之前,请务必认真查看文档:GetX 代码生成 IDEA 插件,超详细功能讲解 ,确保想提的需求,在本插件里面未被实现;上次有个老哥给我连开三个 issue,提的需求都是早已实现的功能。。。
说明
插件地址
插件的功能含义
Model:生成 GetX 的模式
Default:默认模式,生成三个文件:state,logic,view
Easy:简单模式,生成俩个文件:logic,view
Module Name:模块的名称,请使用大驼峰或小驼峰命名
插件详细功能说明,请查阅:GetX 代码生成 IDEA 插件,超详细功能讲解
安装
在设置里面选择:Plugins —> 输入“getx”搜索 —> 选择名字为:“GeX” —> 然后安装 —> 最后记得点击下“Apply”
效果图
Alt + Enter : 可以选择包裹 Widget,有四种可选:GetBuilder、GetBuilder(Auto Dispose),Obx、GetX,大大方便开发哟(^U^)ノ~YO
如果你发现某个页面,你的 GetXController 无法回收,可以使用 GetBuilder(Auto Dispose)Wrap 你的 Widget
计数器 效果图
实现 首先,当然是实现一个简单的计数器,来看 GetX 怎么将逻辑层和界面层解耦的
来看下生成的默认代码,默认代码十分简单,详细解释放在俩种状态管理里
1 2 3 4 5 6 import 'package:get/get.dart' ;class CounterGetLogic extends GetxController {}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 import 'package:flutter/material.dart' ;import 'package:get/get.dart' ;import 'logic.dart' ;class CounterGetPage extends StatelessWidget { final logic = Get.put(CounterGetLogic()); @override Widget build(BuildContext context) { return Container(); } }
简单状态管理
GetBuilder:这是一个极其轻巧的状态管理器,占用资源极少!
logic:先来看看 logic 层
因为是处理页面逻辑的,加上 Controller 单词过长,也防止和 Flutter 自带的一些控件控制器弄混,所以该层用logic
结尾,这里就定为了logic
层
当然这点随个人意向,写 Event,Controller 均可(插件生成代码,支持自定义通用后缀)
1 2 3 4 5 6 7 8 9 class CounterEasyLogic extends GetxController { var count = 0 ; void increase() { ++count; update(); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class CounterEasyPage extends StatelessWidget { final logic = Get.put(CounterEasyLogic()); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('计数器-简单式' )), body: Center( child: GetBuilder<CounterEasyLogic>(builder: (logic) { return Text( '点击了 ${logic.count} 次' , style: TextStyle(fontSize: 30.0 ), ); }), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: Icon(Icons.add), ), ); } }
分析下:GetBuilder 这个方法
init:虽然上述代码没用到,但是,这个参数是存在在 GetBuilder 中的,因为在加载变量的时候就使用Get.put()
生成了CounterEasyGetLogic
对象,GetBuilder 会自动查找该对象,所以,就可以不使用 init 参数
builder:方法参数,拥有一个入参,类型便是 GetBuilder 所传入泛型的类型
initState,dispose 等:GetBuilder 拥有 StatefulWidget 所有周期回调,可以在相应回调内做一些操作
响应式状态管理
当数据源变化时,将自动执行刷新组件的方法
logic 层
这里变量数值后写.obs
操作,是说明定义了该变量为响应式变量,当该变量数值变化时,页面的刷新方法将自动刷新
基础类型,List,类都可以加.obs
,使其变成响应式变量
1 2 3 4 5 6 7 class CounterRxLogic extends GetxController { var count = 0. obs; void increase() => ++count; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class CounterRxPage extends StatelessWidget { final logic = Get.put(CounterRxLogic()); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('计数器-响应式' )), body: Center( child: Obx(() { return Text( '点击了 ${logic.count.value} 次' , style: TextStyle(fontSize: 30.0 ), ); }), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: Icon(Icons.add), ), ); } }
可以发现刷新组件的方法极其简单:Obx()
,这样可以愉快的到处写定点刷新操作了
Obx()方法刷新的条件
只有当响应式变量的值发生变化时,才会会执行刷新操作,当某个变量初始值为:“test”,再赋值为:“test”,并不会执行刷新操作
当你定义了一个响应式变量,该响应式变量改变时,包裹该响应式变量的 Obx()方法才会执行刷新操作,其它的未包裹该响应式变量的 Obx()方法并不会执行刷新操作,Cool!
来看下如果把整个类对象设置成响应类型,如何实现更新操作呢?
下面解释来自官方 README 文档
这里尝试了下,将整个类对象设置为响应类型,当你改变了类其中一个变量,然后执行更新操作,只要包裹了该响应类变量的Obx(),都会实行刷新操作
,将整个类设置响应类型,需要结合实际场景使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class User { User({this .name = '' , this .age = 0 }); String name; int age; } final user = User().obs;user.update( (user) { user.name = 'Jonny' ; user.age = 18 ; }); user(User(name: 'João' , age: 35 )); Obx(()=> Text("Name ${user.value.name} : Age: ${user.value.age} " )); user().name;
总结 分析
Obx 是配合 Rx 响应式变量使用、GetBuilder 是配合 update 使用:请注意,这完全是俩套定点刷新控件的方案
区别:前者响应式变量变化,Obx 自动刷新;后者需要使用 update 手动调用刷新
每一个响应式变量,都需要生成对应的GetStream
,占用资源大于基本数据类型,会对内存造成一定压力
GetBuilder
内部实际上是对 StatefulWidget 的封装,所以占用资源极小
使用场景
一般来说,对于大多数场景都是可以使用响应式变量的
但是,在一个包含了大量对象的 List,都使用响应式变量,将生成大量的GetStream
,必将对内存造成较大的压力,该情况下,就要考虑使用简单状态管理了
总的来说:推荐 GetBuilder 和 update 配合的写法
GetBuilder 内置回收 GetxController 的功能,能避免一些无法自动回收 GetxController 的坑爹问题
使用GetBuilder的自动回收:GetBuilder需要设置assignId: true;或使用插件一键Wrap Widget:GetBuilder(Auto Dispose)
使用 Obx,相关变量定义初始化以及实体更新和常规写法不同,会对初次接触该框架的人,造成很大的困扰
getx 的 IDEA 插件现已支持一键 Wrap Widget 生成 GetBuilder,可以一定程度上提升你的开发效率
跨页面交互
跨页面交互,在复杂的场景中,是非常重要的功能,来看看 GetX 怎么实现跨页面事件交互的
效果图
体验一下
Cool,这才是真正的跨页面交互!下级页面能随意调用上级页面事件,且关闭页面后,下次重进,数据也很自然重置了(全局 Bloc 不会重置,需要手动重置)
实现 页面一 常规代码
logic
这里的自增事件,是供其它页面调用的,该页面本身没使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 class GetJumpOneLogic extends GetxController { var count = 0 ; void toJumpTwo() { Get.toNamed(RouteConfig.getJumpTwo, arguments: {'msg' : '我是上个页面传递过来的数据' }); } void increase() { count = ++count; update(); } }
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 class GetJumpOnePage extends StatelessWidget { final logic = Get.put(GetJumpOneLogic()); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar(title: Text('跨页面-One' )), floatingActionButton: FloatingActionButton( onPressed: () => logic.toJumpTwo(), child: const Icon(Icons.arrow_forward_outlined), ), body: Center( child: GetBuilder<GetJumpOneLogic>( builder: (logic) { return Text('跨页面-Two点击了 ${logic.count} 次' , style: TextStyle(fontSize: 30.0 )); }, ), ), ); } }
页面二 这个页面就是重点了
logic
将演示怎么调用前一个页面的事件
怎么接收上个页面数据
请注意,GetxController
包含比较完整的生命周期回调,可以在onInit()
接受传递的数据;如果接收的数据需要刷新到界面上,请在onReady
回调里面接收数据操作,onReady
是在addPostFrameCallback
回调中调用,刷新数据的操作在onReady
进行,能保证界面是初始加载完毕后才进行页面刷新操作的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class GetJumpTwoLogic extends GetxController { var count = 0 ; var msg = '' ; @override void onReady() { var map = Get.arguments; msg = map['msg' ]; update(); super .onReady(); } void increase() { count = ++count; update(); } }
view
加号的点击事件,点击时,能实现俩个页面数据的变换
重点来了,这里通过Get.find()
,获取到了之前实例化 GetXController,获取某个模块的 GetXController 后就很好做了,可以通过这个 GetXController 去调用相应的事件,也可以通过它,拿到该模块的数据!
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 class GetJumpTwoPage extends StatelessWidget { final oneLogic = Get.find<GetJumpOneLogic>(); final twoLogic = Get.put(GetJumpTwoLogic()); @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar(title: Text('跨页面-Two' )), floatingActionButton: FloatingActionButton( onPressed: () { oneLogic.increase(); twoLogic.increase(); }, child: const Icon(Icons.add), ), body: Center( child: Column(mainAxisSize: MainAxisSize.min, children: [ GetBuilder<GetJumpTwoLogic>( builder: (logic) { return Text('跨页面-Two点击了 ${twoLogic.count} 次' , style: TextStyle(fontSize: 30.0 )); }, ), GetBuilder<GetJumpTwoLogic>( builder: (logic) { return Text('传递的数据:${twoLogic.msg} ' , style: TextStyle(fontSize: 30.0 )); }, ), ]), ), ); } }
总结 GetX 这种的跨页面交互事件,真的是非常简单了,侵入性也非常的低,不需要在主入口配置什么,在复杂的业务场景下,这样简单的跨页面交互方式,就能实现很多事了
进阶吧!计数器
我们可能会遇到过很多复杂的业务场景,在复杂的业务场景下,单单某个模块关于变量的初始化操作可能就非常多,在这个时候,如果还将 state(状态层)和 logic(逻辑层)写在一起,维护起来可能看的比较晕
这里将状态层和逻辑层进行一个拆分,这样在稍微大一点的项目里使用 GetX,也能保证结构足够清晰了!
在这里就继续用计数器举例吧!
实现 此处需要划分三个结构了:state(状态层),logic(逻辑层),view(界面层)
这里使用插件生成下模板代码
Model:选择 Default(默认)
Function:useFolder(默认选中)
来看下生成的模板代码
1 2 3 4 5 6 class GetCounterHighState { GetCounterHighState() { } }
1 2 3 4 5 6 7 8 import 'package:get/get.dart' ;import 'state.dart' ;class GetCounterHighLogic extends GetxController { final GetCounterHighState state = GetCounterHighState(); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 import 'package:flutter/material.dart' ;import 'package:get/get.dart' ;import 'logic.dart' ;class GetCounterHighPage extends StatelessWidget { final logic = Get.put(GetCounterHighLogic()); final state = Get.find<GetCounterHighLogic>().state; @override Widget build(BuildContext context) { return Container(); } }
为什么写成这样三个模块,需要把 State 单独提出来,请速速浏览下方
改造
state
这里使用划分出来的 state 层,来统一管理所有的状态变量
涉及到状态变量定义和 Logic 层彻底分开
1 2 3 4 5 6 7 8 class GetCounterHighState { late int count; GetCounterHighState() { count = 0 ; } }
logic
逻辑层就比较简单,需要注意的是:开始时需要实例化状态类
1 2 3 4 5 6 7 8 9 10 class GetCounterHighLogic extends GetxController { final GetCounterHighState state = GetCounterHighState(); void increase() { state.count = ++state.count; update(); } }
view
实际上 view 层,和之前的几乎没区别,区别的是把状态层给独立出来了
因为CounterHighGetLogic
被实例化,所以直接使用Get.find<CounterHighGetLogic>()
就能拿到刚刚实例化的逻辑层,然后拿到 state,使用单独的变量接收下
ok,此时:logic 只专注于触发事件交互,state 只专注数据
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 class GetCounterHighPage extends StatelessWidget { final logic = Get.put(GetCounterHighLogic()); final state = Get.find<GetCounterHighLogic>().state; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('计数器-进阶版' )), body: Center( child: GetBuilder<GetCounterHighLogic>( builder: (logic) { return Text( '点击了 ${state.count} 次' , style: TextStyle(fontSize: 30.0 ), ); }, ), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: Icon(Icons.add), ), ); } }
对比
看了上面的改造,屏幕前的你可能想吐槽了:坑比啊,之前简简单单的逻辑层,被拆成俩个,还搞得这么麻烦,你是猴子请来的逗比吗?
大家先别急着吐槽,当业务过于复杂,state 层,也是会维护很多东西的,让我们看看下面的一个小栗子,下面实例代码是不能直接运行的,想看详细运行代码,请查看项目:flutter_use
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 class MainState { late int selectedIndex; late bool isUnfold; late bool isScale; late List <BtnInfo> list; late List <BtnInfo> itemList; late List <Widget> pageList; late PageController pageController; MainState() { selectedIndex = 0 ; isUnfold = false ; isScale = false ; pageList = [ KeepAlivePage(FunctionPage()), KeepAlivePage(ExamplePage()), KeepAlivePage(SettingPage()), ]; itemList = [ BtnInfo( title: "功能" , icon: Icon(Icons.bubble_chart), ), BtnInfo( title: "范例" , icon: Icon(Icons.opacity), ), BtnInfo( title: "设置" , icon: Icon(Icons.settings), ), ]; pageController = PageController(); } }
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 class MainLogic extends GetxController { final state = MainState(); @override void onInit() { InitConfig.initApp(Get.context); super .onInit(); } void switchTap(int index) { state.selectedIndex = index; state.pageController.jumpToPage(index); update(); } void onUnfold(bool isUnfold) { state.isUnfold = !state.isUnfold; update(); } void onScale(bool isScale) { state.isScale = !state.isScale; update(); initWindow(scale: isScale ? 1.25 : 1.0 ); } }
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 class MainPage extends StatelessWidget { final MainLogic logic = Get.put(MainLogic()); final MainState state = Get.find<MainLogic>().state; @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: Row(children: [ GetBuilder<MainLogic>( builder: (logic) { return SideNavigation( selectedIndex: state.selectedIndex, isUnfold: state.isUnfold, isScale: state.isScale, sideItems: state.itemList, onItem: (index) => logic.switchTap(index), onUnfold: (isUnfold) => logic.onUnfold(isUnfold), onScale: (isScale) => logic.onScale(isScale), ); }, ), Expanded( child: PageView.builder( physics: NeverScrollableScrollPhysics(), itemCount: state.pageList.length, itemBuilder: (context, index) => state.pageList[index], controller: state.pageController, ), ) ]), ); } }
从上面可以看出,state 层里面的状态已经较多了,当某些模块涉及到大量的:提交表单数据,跳转数据,展示数据等等,state 层的代码会相当的多,相信我,真的是非常多,一旦业务发生变更,还要经常维护修改,就蛋筒了
在复杂的业务下,将状态层(state)和业务逻辑层(logic)分开,绝对是个明智的举动
最后
该模块的效果图就不放了,和上面计数器效果一模一样,想体验一下,可点击:体验一下
简单的业务模块,可以使用俩层结构:logic,view;复杂的业务模块,推荐使用三层结构:state,logic,view
Binding 的使用 说明 大家可能发现了,插件上增加了 addBinding 的功能
再加上 getx 的 demo 也用了 binding,想必各位靓仔就非常想使用这个功能
这个功能实际的作用非常简单
统一管理单模块使用的 GetXController
binding 模块需要在 getx 路由页面进行绑定;进入页面的时候,统一懒注入 binding 模块的 GetXController
这样做当然有好处
可以统一管理复杂模块的多个 GetXController
请注意
不建议在 Get.to()方法里面进行 binding 绑定
如果存在多个页面跳转到存在 binding 页面,你的每个 Get.to()方法都需要绑定
这样极其容易出 bug,对后面接盘的人,十分不友好
使用 binding,你理应使用 getx 的命名路由
郑重申明:不使用 binding,并不会对功能有任何的影响
使用
首先必须搭建好路由模块
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 void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return GetMaterialApp( initialRoute: RouteConfig.testOne, getPages: RouteConfig.getPages, ); } } class RouteConfig { static const String testOne = "/testOne" ; static const String testTwo = "/testOne/testTwo" ; static final List <GetPage> getPages = [ GetPage( name: testOne, page: () => TestOnePage(), binding: TestOneBinding(), ), GetPage( name: testTwo, page: () => TestTwoPage(), binding: TestTwoBinding(), ), ]; }
创建页面模块
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 class TestOneLogic extends GetxController { void jump() => Get.toNamed(RouteConfig.testTwo); } class TestOnePage extends StatelessWidget { final logic = Get.find<TestOneLogic>(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('页面一' )), body: Center(child: Text('页面一' , style: TextStyle(fontSize: 30.0 ))), floatingActionButton: FloatingActionButton( onPressed: () => logic.jump(), child: Icon(Icons.arrow_forward), ), ); } } class TestOneBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => TestOneLogic()); } }
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 class TestTwoLogic extends GetxController {} class TestTwoPage extends StatelessWidget { final logic = Get.find<TestTwoLogic>(); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('页面二' )), body: Center(child: Text('页面二' , style: TextStyle(fontSize: 30.0 ))), ); } } class TestTwoBinding extends Bindings { @override void dependencies() { Get.lazyPut(() => TestTwoLogic()); } }
总结 这边我写了一个极其简单的范例,仅仅是个跳转页面的功能,我觉得,应该可以展示 binding 的功能了
就是统一管理某个模块需要注入的多个 GetXController
请注意,该注入是懒注入,只有使用了 find + 对应的泛型,才会被真正的注入的 getx 的全局 map 实例里
实际上,手动写 binding 文件,还是有点麻烦,写了 binding,view 层的使用也需要做相应的变动
铁汁们,为了帮你们节省点开发时间,这点浪费你们生命且没什么技术含量的事情,已经在插件里帮你完成
有需要的,选中 addBinding 功能即可
GetPage 里面绑定 binding 的操作,只能麻烦你们自己动下手了,项目结构千变万化,这玩意没法定位
路由管理 GetX 实现了一套用起来十分简单的路由管理,可以使用一种极其简单的方式导航,也可以使用命名路由导航
关于简单路由和命名路由的区别
命名路由
在 web 上,可以直接通过命名的 url 直接导航页面
实现路由拦截的操作,举一个官方文档的例子:很轻松的实现了一个未登录,跳转登录页面功能
1 2 3 4 5 6 7 8 9 10 GetStorage box = GetStorage(); GetMaterialApp( getPages: [ GetPage(name: '/' , page:(){ return box.hasData('token' ) ? Home() : Login(); }) ] )
简单路由
1 2 3 4 5 6 7 8 9 10 11 12 13 void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return GetMaterialApp( home: MainPage(), ); } }
路由的相关使用
使用是非常简单,使用 Get.to()之类 api 即可,此处简单演示,详细 api 说明,放在本节结尾
命名路由导航 这里是推荐使用命名路由导航的方式
统一管理起了所有页面
在 app 中可能感受不到,但是在 web 端,加载页面的 url 地址就是命名路由你所设置字符串,也就是说,在 web 中,可以直接通过 url 导航到相关页面
下面说明下,如何使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return GetMaterialApp( initialRoute: RouteConfig.main, getPages: RouteConfig.getPages, ); } }
RouteConfig 类
下面是我的相关页面,和其映射的页面,请根据自己的页面进行相关编写
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 67 68 69 70 71 72 73 74 75 76 77 78 class RouteConfig { static const String main = "/" ; static const String smartDialog = "/smartDialog" ; static const String himalaya = "/himalaya" ; static const String dialog = "/dialog" ; static const String blCubitCounterPage = "/blCubitCounterPage" ; static const String blBlocCounterPage = "/blBlocCounterPage" ; static const String cubitSpanOne = "/cubitSpanOne" ; static const String cubitSpanTwo = "/cubitSpanOne/cubitSpanTwo" ; static const String streamPage = "/streamPage" ; static const String blCustomBuilderPage = "/blCustomBuilderPage" ; static const String counterEasyCPage = "/counterEasyCPage" ; static const String testLayout = "/testLayout" ; static const String getCounterRx = "/getCounterRx" ; static const String getCounterEasy = "/counterEasyGet" ; static const String getCounterHigh = "/counterHighGet" ; static const String getJumpOne = "/jumpOne" ; static const String getJumpTwo = "/jumpOne/jumpTwo" ; static const String getCounterBinding = "/getCounterBinding" ; static const String counterEasyXBuilderPage = "/counterEasyXBuilder" ; static const String counterEasyXEbxPage = "/counterEasyXEbx" ; static const String proEasyCounterPage = "/proEasyCounterPage" ; static const String proHighCounterPage = "/proHighCounterPage" ; static const String proSpanOnePage = "/proSpanOnePage" ; static const String proSpanTwoPage = "/proSpanOnePage/proSpanTwoPage" ; static const String testNotifierPage = "/testNotifierPage" ; static const String customBuilderPage = "/customBuilderPage" ; static const String counterEasyPPage = "/counterEasyPPage" ; static const String counterGlobalEasyPPage = "/counterGlobalEasyPPage" ; static final List <GetPage> getPages = [ GetPage(name: main, page: () => MainPage()), GetPage(name: dialog, page: () => DialogPage()), GetPage(name: blCubitCounterPage, page: () => BlCubitCounterPage()), GetPage(name: blBlocCounterPage, page: () => BlBlocCounterPage()), GetPage(name: streamPage, page: () => StreamPage()), GetPage(name: blCustomBuilderPage, page: () => BlCustomBuilderPage()), GetPage(name: counterEasyCPage, page: () => CounterEasyCPage()), GetPage(name: testLayout, page: () => TestLayoutPage()), GetPage(name: smartDialog, page: () => SmartDialogPage()), GetPage(name: cubitSpanOne, page: () => CubitSpanOnePage()), GetPage(name: cubitSpanTwo, page: () => CubitSpanTwoPage()), GetPage(name: getCounterRx, page: () => GetCounterRxPage()), GetPage(name: getCounterEasy, page: () => GetCounterEasyPage()), GetPage(name: getCounterHigh, page: () => GetCounterHighPage()), GetPage(name: getJumpOne, page: () => GetJumpOnePage()), GetPage(name: getJumpTwo, page: () => GetJumpTwoPage()), GetPage( name: getCounterBinding, page: () => GetCounterBindingPage(), binding: GetCounterBinding(), ), GetPage(name: counterEasyXBuilderPage, page: () => EasyXCounterPage()), GetPage(name: counterEasyXEbxPage, page: () => EasyXEbxCounterPage()), GetPage(name: himalaya, page: () => HimalayaPage()), GetPage(name: proEasyCounterPage, page: () => ProEasyCounterPage()), GetPage(name: proHighCounterPage, page: () => ProHighCounterPage()), GetPage(name: proSpanOnePage, page: () => ProSpanOnePage()), GetPage(name: proSpanTwoPage, page: () => ProSpanTwoPage()), GetPage(name: testNotifierPage, page: () => TestNotifierPage()), GetPage(name: customBuilderPage, page: () => CustomBuilderPage()), GetPage(name: counterEasyPPage, page: () => CounterEasyPPage()), GetPage(name: counterGlobalEasyPPage, page: () => CounterGlobalEasyPPage()), ]; }
路由 API 请注意命名路由,只需要在 api 结尾加上Named
即可,举例:
默认:Get.to(SomePage());
命名路由:Get.toNamed(“/somePage”);
详细 Api 介绍,下面内容来自 GetX 的 README 文档,进行了相关整理
1 2 3 Get.to(NextScreen()); Get.toNamed("/NextScreen" );
关闭 SnackBars、Dialogs、BottomSheets 或任何你通常会用 Navigator.pop(context)关闭的东西
进入下一个页面,但没有返回上一个页面的选项(用于 SplashScreens,登录页面等)
1 2 3 Get.off(NextScreen()); Get.offNamed("/NextScreen" );
进入下一个界面并取消之前的所有路由(在购物车、投票和测试中很有用)
1 2 3 Get.offAll(NextScreen()); Get.offAllNamed("/NextScreen" );
只要发送你想要的参数即可。Get 在这里接受任何东西,无论是一个字符串,一个 Map,一个 List,甚至一个类的实例。
1 2 3 Get.to(NextScreen(), arguments: 'Get is the best' ); Get.toNamed("/NextScreen" , arguments: 'Get is the best' );
在你的类或控制器上:
1 2 3 var data = await Get.to(Payment());var data = await Get.toNamed("/payment" );
1 2 3 4 Get.back(result: 'success' ); if (data == 'success' ) madeAnything();
如果你不想使用 GetX 语法,只要把 Navigator(大写)改成 navigator(小写),你就可以拥有标准导航的所有功能,而不需要使用 context,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 Navigator.of(context).push( context, MaterialPageRoute( builder: (BuildContext context) { return HomePage(); }, ), ); navigator.push( MaterialPageRoute( builder: (_) { return HomePage(); }, ), ); Get.to(HomePage());
动态网页链接
这是一个非常重要的功能,在 web 端,可以保证通过url传参数到页面
里
Get 提供高级动态 URL,就像在 Web 上一样。Web 开发者可能已经在 Flutter 上想要这个功能了,Get 也解决了这个问题。
1 2 Get.offAllNamed("/NextScreen?device=phone&id=354&name=Enzo" );
在你的 controller/bloc/stateful/stateless 类上:
1 2 3 4 5 print (Get.parameters['id' ]);print (Get.parameters['name' ]);
你也可以用 Get 轻松接收 NamedParameters。
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 void main() { runApp( GetMaterialApp( initialRoute: '/' , getPages: [ GetPage( name: '/' , page: () => MyHomePage(), ), GetPage( name: '/profile/' , page: () => MyProfile(), ), GetPage( name: '/profile/:user' , page: () => UserProfile(), ), GetPage( name: '/third' , page: () => Third(), transition: Transition.cupertino ), ], ) ); }
发送命名路由数据
1 2 Get.toNamed("/profile/34954" );
在第二个页面上,通过参数获取数据
1 2 3 print (Get.parameters['user' ]);
现在,你需要做的就是使用 Get.toNamed()来导航你的命名路由,不需要任何 context(你可以直接从你的 BLoC 或 Controller 类中调用你的路由),当你的应用程序被编译到 web 时,你的路由将出现在 URL 中。
资源释放
关于 GetxController 的资源释放,这个栏目的内容相当重要!
资源未释放的场景 在我们使用 GetX 的时候,可能没什么 GetxController 未被释放的感觉,这种情况,是因为我们一般都是用了 getx 的那一套路由跳转 api(Get.to、Get.toName…)之类:使用 Get.toName,肯定需要使用 GetPage;如果使用 Get.to,是不需要在 GetPage 中注册的,Get.to 的内部有一个添加到 GetPageRoute 的操作
通过上面会在 GetPage 注册可知,说明在我们跳转页面的时候,GetX 会拿你到页面信息存储起来,加以管理,下面俩种场景会导致 GetxController 无法释放
GetxController 可被自动释放的条件
GetPage+Get.toName 配套使用,可释放
直接使用 Get.to,可释放
GetxController 无法被自动释放场景
未使用 GetX 提供的路由跳转:直接使用原生路由 api 的跳转操作
这样会直接导致 GetX 无法感知对应页面 GetxController 的生命周期,会导致其无法释放
1 2 3 4 5 Navigator.push( context, MaterialPageRoute(builder: (context) => XxxxPage()), );
由此,可从上面可以看出,GetxController 无法被释放的场景:不使用 GetX 路由
最优解 这里有个最优解方案,就算你不使用 Getx 路由,也能很轻松回收各个页面的 GetXController,感谢 @法的空间 在评论里指出
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 void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( home: HomePage, navigatorObservers: [GetXRouterObserver()], ); } } class GetXRouterObserver extends NavigatorObserver { @override void didPush(Route<dynamic > route, Route<dynamic >? previousRoute) { RouterReportManager.reportCurrentRoute(route); } @override void didPop(Route<dynamic > route, Route<dynamic >? previousRoute) async { RouterReportManager.reportRouteDispose(route); } }
讲真的,这个原理其实很简单,但是思路很有趣;大家点进reportCurrentRoute
和 reportRouteDispose
这俩个方法,大概就知道是怎么回事了
reportCurrentRoute
就是让当前的路由标定给 GetX
当我们进入一个页面的时候,相应 GetXController 会进行初始化,最终会调用 _startController<S>({String? tag})
方法
_startController
中会调用RouterReportManager.appendRouteByCreate(i)
,将注入的 GetXController 都保存起来
保存在一个 map 中,key 为当前路由route
,value 为 HashSet,可以保存多个 GetXController
ok,路由关闭的时候,在reportRouteDispose
方法中回收,key 为当前route
,遍历 value 中所有的 GetXController 回收
我 giao,基于这种思路,大家能干很多事了!!!
折中方案 如果上面的最优解没法帮你解决 GetXController 的回收问题,你可能就遇到特殊的场景了,一般来说,分析分析你自己的代码,基本都能分析出来
如果懒得分析原因,就试试下面这种折中方案吧;颗粒度极小,针对单页面维度解决
这边我模拟了上面场景,写了一个解决方案
1 2 3 4 5 Navigator.push( Get.context, MaterialPageRoute(builder: (context) => AutoDisposePage()), );
演示页面
这地方地方必须要使用 StatefulWidget,因为在这种情况,无法感知生命周期,就需要使用 StatefulWidget 生命周期
在 dispose 回调处,把当前 GetxController 从整个 GetxController 管理链中删除即可
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 class AutoDisposePage extends StatefulWidget { @override _AutoDisposePageState createState() => _AutoDisposePageState(); } class _AutoDisposePageState extends State <AutoDisposePage > { final AutoDisposeLogic logic = Get.put(AutoDisposeLogic()); @override Widget build(BuildContext context) { return BaseScaffold( appBar: AppBar(title: const Text('计数器-自动释放' )), body: Center( child: Obx( () => Text('点击了 ${logic.count.value} 次' , style: TextStyle(fontSize: 30.0 )), ), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: const Icon(Icons.add), ), ); } @override void dispose() { Get.delete<AutoDisposeLogic>(); super .dispose(); } } class AutoDisposeLogic extends GetxController { var count = 0. obs; void increase() => ++count; }
看到这,你可能会想,啊这!怎么这么麻烦,我怎么还要写 StatefulWidget,好麻烦!
各位放心,这个问题,我也想到了,我特地在插件里面加上了自动回收的功能
如果你写的页面无法被回收,记得勾选 autoDispose
怎么判断页面的 GetxController 是否能被回收呢?实际上很简单,上面的未被释放的场景已经描述的比较清楚了,不清楚的话,就再看看
来看下代码,default 模式一样可以的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class AutoDisposePage extends StatefulWidget { @override _AutoDisposePageState createState() => _AutoDisposePageState(); } class _AutoDisposePageState extends State <AutoDisposePage > { final AutoDisposeLogic logic = Get.put(AutoDisposeLogic()); @override Widget build(BuildContext context) { return Container(); } @override void dispose() { Get.delete<AutoDisposeLogic>(); super .dispose(); } }
1 2 3 4 class AutoDisposeLogic extends GetxController {}
上面的是个通用解决方法,你不需要额外的引入任何其它的东西;但是这种方案用到了 StatefulWidget,代码多了一大坨,让我有点膈应
鄙人有着相当的强迫症,想了很久,从外部入手,我就写了一个通用控件,来对相应的 GetXController 进行回收
GetBindWidget
本控件含义:将 GetXController 和当前页面的生命周期绑定,页面关闭时,自动回收
该控件可以回收单个 GetXController(bind 参数),可以加上对应 tag(tag 参数);也可以回收多个 GetXController(binds),可以加上多个 tag(tags 参数,请和 binds 一 一 对应;无 tag 的 GetXController 的,tag 可以写成空字符:””)
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 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 import 'package:flutter/material.dart' ;import 'package:get/get.dart' ;class GetBindWidget extends StatefulWidget { const GetBindWidget({ Key? key, this .bind, this .tag, this .binds, this .tags, required this .child, }) : assert ( binds == null || tags == null || binds.length == tags.length, 'The binds and tags arrays length should be equal\n' 'and the elements in the two arrays correspond one-to-one' , ), super (key: key); final GetxController? bind; final String? tag; final List <GetxController>? binds; final List <String >? tags; final Widget child; @override _GetBindWidgetState createState() => _GetBindWidgetState(); } class _GetBindWidgetState extends State <GetBindWidget > { @override Widget build(BuildContext context) { return widget.child; } @override void dispose() { _closeGetXController(); _closeGetXControllers(); super .dispose(); } void _closeGetXController() { if (widget.bind == null ) { return ; } var key = widget.bind.runtimeType.toString() + (widget.tag ?? '' ); GetInstance().delete(key: key); } void _closeGetXControllers() { if (widget.binds == null ) { return ; } for (var i = 0 ; i < widget.binds!.length; i++) { var type = widget.binds![i].runtimeType.toString(); if (widget.tags == null ) { GetInstance().delete(key: type); } else { var key = type + (widget.tags?[i] ?? '' ); GetInstance().delete(key: key); } } } }
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 class TestPage extends StatelessWidget { final logic = Get.put(TestLogic()); @override Widget build(BuildContext context) { return GetBindWidget( bind: logic, child: Container(), ); } } class TestPage extends StatelessWidget { final logicOne = Get.put(TestLogic(), tag: 'one' ); final logicTwo = Get.put(TestLogic()); final logicThree = Get.put(TestLogic(), tag: 'three' ); @override Widget build(BuildContext context) { return GetBindWidget( binds: [logicOne, logicTwo, logicThree], tags: ['one' , '' , 'three' ], child: Container(), ); } } [GETX] Instance "TestLogic" has been created with tag "one" [GETX] Instance "TestLogic" with tag "one" has been initialized [GETX] Instance "TestLogic" has been created [GETX] Instance "TestLogic" has been initialized [GETX] Instance "TestLogic" has been created with tag "three" [GETX] Instance "TestLogic" with tag "three" has been initialized [GETX] "TestLogicone" onDelete() called [GETX] "TestLogicone" deleted from memory [GETX] "TestLogic" onDelete() called [GETX] "TestLogic" deleted from memory [GETX] "TestLogicthree" onDelete() called [GETX] "TestLogicthree" deleted from memory
一些问题汇总
如果使用中,有比较坑的问题,希望大家在评论里提出来,我会在这个栏目汇总一下
无法跳转重复页面
另一种表现形式:使用 Get.to(Get.toName)在系统 Dialog 上跳转页面,未关闭 Dialog;返回,再跳转,会出现无法跳转的情况
debug 了下 to 方法内部的运行,发现他用了一个 preventDuplicates 参数,限制跳转重复页面
为什么这样做?
优点:能解决多次点击跳转按钮,跳转多个重复页面的问题
缺点:限制了复杂业务跳转重复页面的场景
当然上面的缺点也不算是缺点,毕竟已经给了参数可以控制
1 2 3 4 Get.to(XxxxPage(), preventDuplicates: false ); Get.toNamed('xxx' , preventDuplicates: false );
使用 PageView 时,所有 PageView 页面控制器,全被初始化问题
大家使用 PageView,添加 PageView 页面,PageView 页面用 GetX 构成,会发现所有的 PageView 页面控制器全被初始化了!并不是切换到某个页面时,对应页面的控制器才被初始化!
PageView 切换到某个页面的时候,才会调用对应 Page 页面的 build 方法;对于PageView页面,控制器的注入过程,不能写在类中了,需要将其移入到build方法中初始化。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class CounterEasyGetPage extends StatelessWidget { final CounterEasyGetLogic logic = Get.put(CounterEasyGetLogic()); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('计数器-简单式' )), body: Center( child: GetBuilder<CounterEasyGetLogic>( builder: (logicGet) => Text( '点击了 ${logicGet.count} 次' , style: TextStyle(fontSize: 30.0 ), ), ), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: const Icon(Icons.add), ), ); } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 class CounterEasyGetPage extends StatelessWidget { @override Widget build(BuildContext context) { final CounterEasyGetLogic logic = Get.put(CounterEasyGetLogic()); return Scaffold( appBar: AppBar(title: const Text('计数器-简单式' )), body: Center( child: GetBuilder<CounterEasyGetLogic>( builder: (logicGet) => Text( '点击了 ${logicGet.count} 次' , style: TextStyle(fontSize: 30.0 ), ), ), ), floatingActionButton: FloatingActionButton( onPressed: () => logic.increase(), child: const Icon(Icons.add), ), ); } }
大家如果觉得手动移太麻烦的话,也可以选中插件的 isPageView 功能
最后 相关地址
系列文章
引流了,手动滑稽.png