getx就像java web开发里的spring,什么东西都帮你考虑到了。能够快速搭建起项目。
但getx真的好用吗?当不了解原理的时候,你可能就用错了。甚至让你百思不得其解。记录一下我这个碰到的坑。
最近刚接手了一个二手项目,项目用到了Getx,对是二手的,如果是我,肯定不会选它,但毕竟是二手的,我也没得选择。
首先说明,我并不是一个Getx小白,早在3年前,刚接触Flutter的时候,我最早就用上了Getx,且完成了我的第一个项目,很丝滑,没碰到什么坑。
但这次的二手项目,碰到一个问题,让我排查了整整一天。
首先这个项目是用InitBinding的,将所有的Controller全部注册在InitBingding里面。
例如下面的代码:
GetMaterialApp(
initialBinding: InitialBinding(),
// 添加初始绑定
home: const SimpleSplashPage(),
....
)
class InitialBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<SimpleSplashController>(
() => SimpleSplashController(),
fenix: true,
);
....
}
且在InitialBinding里面所有Controller的fenix: true都是开启的状态
fenix: true 很好用,就是GetBuilder可以自动帮你管理Controller的创建和销毁,当多个GetBuilder绑定同一个Controller时,不会重复创建它,当所有绑定这个Controller的GetBuilder都被销毁时,Controller就会走onClose,并被销毁。
这么看好像没啥问题。但是坑爹的地方就在于 Get.find 这个方法,从字面意思上面理解就是找对应的Controller对它进行操作。看起来挺美好的,因为你不需要BuildContext,你能够在你代码的任意位置通过这个方法找到你需要的Controller对象,你可以用它去刷新UI,省事且方便。
但是坑爹的地方就来了,你发现在你代码里的各个角落都能找到 Get.find的调用,没有任何限制。有人会问,这样有什么问题吗?
问题可她妈大了。这个坑爹的find方法。
它不止是去查找,如果没找到就去创建!!而且这种创建是和Widget生命周期脱钩的!!!
我举个例子:有页面A 和 B,A页面里通过GetBuilder绑定了 ControllerA,一般而言如果页面A销毁了,ControllerA就会被销毁。我们从A页面打开B页面,在B页面我们可以通过Get.find操作并筛选A页面,这样貌似没啥问题。
但是我们换另外一个场景:C页面打开了A,然后A页面打开了B,因为某些原因A页面被关闭了,这个时候在B页面你又通过Get.find去试图做点事情,你可能不是想去刷新A页面,可能某些逻辑例如插入数据库的操作刚刚写在了ControllerA里面,你就这么理所当然的调用了,问题出现了,通过Get.find会创建一个完全与页面A脱钩的ControllerA实例。可能我们需要在ControllerA的onInit里面通过Get.arguments获取到一些参数,这个时候都获取不到了。等下次你再打开A页面时,并不会销毁这个ControllerA实例,而是直接拿来就用了,但某些参数是从Get.arguments里获取的,因为ControllerA已经走了onInit流程,并不会重复去调用,因此这些参数相当于是丢失了。
就是很简单的一个问题,我排查了一上午才找到原因,但是找到问题之后我又犯难了,因为项目里面到处都是Get.find,所有这种调用都是暴雷点。
写到这我不得不吐槽Getx的函数命名方式,就不能给个Get.findOrNull,Get.findOrCreate这种命名吗?(可能有人会骂我,自己不先看文档怪谁?但说好的上手即用呢?)
Getx我不可否认是一个不错的框架,上手很容易,也很符合我们国内的开发习惯,但是上手容易踩坑也容易,不理解其中原理极容易用错,特别是初学者,我总结一下看似很好的但是有坑的设计:
-
BuildContext全局,我们可以通过Get.context在任意的地方调用它,看似是一个很好的设计,例如弹框什么的没有代码限制,初学者狂喜,但实际上坑很大,没有良好的构架意识,你会发现你的代码里导出的都是这个这个东西,而且这东西的滥用非常影响单元测试,例如我看到很多初学者的代码在http组件里调用它,就是想做全局弹框,或许会有人说这样没有什么问题啊,那我问你,你是否写过单元测试?
-
命名很随意,上面我有说过,Get.find的命名问题,其实不止如此,例如GetxController里面有dispose方法,还有onClose方法,如果第一次接触的人会理所当然的热为dispose是销毁方法吧?但实际上是onClose,当你把一些需要销毁的操作放在dispose里面的时候,最后发现不知道怎么就是不调用,看了文档才知道走了onClose,官方Flutter 组件都是走dispose为什么就是你偏偏走onClose啊,傻傻分不清楚。
-
Obx里不放Rx就报错,这看似是很好的设计,是让我们知道,Obx是为刷新服务的,但实际上很烦人,我们在开发阶段都是先写UI的,我们知道这个Widget树下是需要刷新的,但我暂时不想写,或者是之前写了,但是有问题想用静态数据先看看效果,但偏偏这个Obx就报错了,必须把这部分先删掉。像riverpod有专门overlay可以覆盖传入的值,就是考虑到开发者可能需要mock或者写单元测试,但是getx没有。
-
测试用例不好写,再拿Riverpod对比,很多人说Riverpod难用,但当你再研究,你会发现Riverpod的所有设计都是为可测试性服务的,它的理念就是所有Provider都是可以单独写测试用例,所有UI都是可以通过代码Mock数据。为什么要强调不能弹框自由,写个弹框还需要一个变量控制在UI层面去弹?看似繁琐的设计,都是为可测试性让路。但Getx的设计,看似方便了开发者,但是确脱离了规范性。
-
集成容易,剥离难。强而全的框架,当你想剥离它,你会发现异常困难,就像我上面说的当你代码里各个角落都是Get.find,Get.context的时候,每一个删改都可能引项目崩溃,当你用到它,你就再也离不开它。有人会问,为什么要剥离?我举个例子,项目A用了Getx,项目B没有用它,某些实现是项目A里的现在项目迁移到项目B,你会发现项目A深深的绑定了Getx,如果项目B想用它就也必须引入Getx,且还需要大量改造,例如一些全局变量的不正规应用。又有人会问,其他框架没这个问题吗?有,但是没有Getx这么全方位。项目大了,是可以容忍不同模块用不同状态管理的。但你一个做状态管理的,还管这管那的,是不是越界了?
Em...
以上纯属于“自己不会用怪框架系列”,getx是个好框架,只是自己不会用,是我自己菜了,各位看看就行了。