前言
架构
这儿讲的架构并不是MVC, MVP, MVVM之类的. 其实像React Native, Flutter这种声明式UI, 架构上天生便是MVVM. 当然你还能够搞点花样, 加上Redux, 弄成个MVI也行, 只不过我三年的React Native经验告诉我, Redux不太好用. 所以我运用MVVM就行了.
这儿讲的架构, 主要是充分使用Getx来架构整个App, 更倾向于全体架构上的一些细节, 而不是着重在MVP这样的分层.
Getx
Getx是一个很强壮的库, 如同也是pub.dev上like数最多的一个库.
既然有这么多人Like它, 那我就也来分享一下我的运用经验吧.
Getx自己主要是分成四部分的:
- Router (路由, 跳到某页去)
- DI (依靠注入, 相似Dagger, Koin)
- State办理 (办理widget的state, 相似RN中的Redux, 或Android中的Presenter/ViewModel)
- Utilty (各种东西类, 东西办法) 下面的讲解也是环绕这几部分来的
二. 路由
1. 为何不必Flutter自己的Router体系
Flutter自己是有router体系的, 但缺陷也明显 1). 这个自带的router的named router有局限性, flutter并不推荐用. 可是named router明显能快速对接deep links, 所以这种局限性很不利于咱们开发
2). 功用有限. 像咱们需求的off, offAll这些功用就没有 (getx里有)
3). 运用时还需求有一个context实例. 但咱们并不是随时随地都持有一个context的, 这也局限了咱们的运用场景.
4). 运用起来麻烦
// 这是Flutter自带的router体系
onPressed: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => const SongScreen(song: song)
),
);
// 这是Getx
onPressed: () => Get.to( SongScreen() );
所以我仍是推荐运用Getx的Router体系
2. 路由 (Router)
由于这毕竟不是一篇介绍Getx的入门文, 所以这儿就不多做Getx的讲解, 只做一些要害的阐明, 或是架构上的阐明.
2.1 界说路由
咱们一般要先界说一个Getx的Router. 由于咱们要对接deep link, 即后台给咱们一个yourcompany://page1?id=23
的string, 你能跳到某一页去 (并带上参数id=23). 所以咱们的根本要求便是: 要能经过一个plain string就能知道要跳到哪个页面去, 并支持传参
GetMaterialApp(
initialRoute: "/home",
unknownRoute: GetPage(name: "/404", page: () => const NotFoundPage()),
routingCallback: (routing) { ... } //相当于跳转的监听器
getPages: [
GetPage(name: "/home", page: ()=> const HomePage()),
GetPage(name: "/detail", page: ()=> OneDetailPage()),
-
initialRoute
是主页是哪个 -
unknownRoute
是没找到对应的页面时, 就去这个页面 -
getPages
界说一个Map<String, function>的路由表. 留意page都是函数, 这样咱们就能做到lazy initialization. 要是用GetPage(name: "/home", page: HomePage())
这样的路由表, 那一翻开app, 一切页面都初始化好了, 太浪费资源, 也太慢了. -
routingCallback
是跳转的监听器, 当你跳转到下一页, 或是按back等会调用它. 概况可见这儿.
坑1
许多从web转过来的人, 都喜爱让initRouter界说为”/”. 但在Getx中, 这样做会让unkonwRoute失效. 这是我经过反复研究才找到的一个躲藏bug吧.
也便是说, 为了unknowRoute能成功, 你的initRoute不能是”/”.
2.2 跳转
这儿的跳转功用就很丰厚了, 下面一一讲解
Get.to(NextScreen());
Get.toName("/detail"); //比起to(), 一般运用toNamed()
// 跳转时带参数
Get.toNamed("/router/back/p3?id=100&name=flutter")
// 本页finish, 再跳detail页
Get.offNamed("/detail"); //其实便是说用detail页来replace掉本页
// 运用场景: 当点了"logout"按钮时, 铲除一切页, 再跳到login页去
Get.offAllNamed("login"); //相似Android中的clear_task | new_task
// 后退
Get.back();
留意到Getx的路由跳转是不需求context的, 这样你在任何地方的代码(如ViewModel, Repository, …)都能够写跳转的代码.
跳转时的传参
当然, 若你的参数对错String, 是个一般类, 那就得用argument
, 而不是parameter
// A1). 带Map<String, String>参数
final params = <String, String> {"source": "p1", "value": "230"};
Get.toNamed("/router/back/p2", parameters: params);
// 或用这种办法也行
Get.toNamed("/router/back/p3?id=100&name=flutter")
// A2). 下一页中取出Map<String, String>参数
String source = Get.parameters["source"] ?? "<default>";
String name = Get.parameters["name"] ?? "<default>";
// - - - - - - - - - - - - - - - - - - - - - - - -
// 若参数不是Map<String, String>类型, 就用不了parameter
// 这时就要用 Get.toNamed("..", argument)
// B1). 存入值
final params = Offset(12, 13);
Get.toNamed("/router/back/p3", arguments: params);
// B2). 取出值
final args = Get.arguments;
2.3 路由中间件
Router Middleware能够理解为, 你要跳转时, 就得先去中间件里报个到, 中间件们或是打日志, 或是发现没登录就重定向到登录页去, 或是埋点, …, 总之一切中间件过一遍, 没问题了, 才能真正到达跳转的终点
2.3.1 中间件的callback版本
这个严格来说是个路由的listener, 而不是中间件. 不过每次跳转都会经过它(包含你按back, 或是dismiss掉一个dialog), 所以你能够用它来记录一些比如Page栈的作业, 或是埋点的作业
@override
Widget build(BuildContext context) {
return GetMaterialApp(
initialRoute: "/home",
unknownRoute: GetPage(name: "/404", page: () => const NotFoundPage()),
routingCallback: (routing) {
MyRoutingLifecycle.onRoutingChange(routing);
},
这样我就能够知道跳转的一些细节(都在routing参数里). routing参数的类型是Routing
, 要害源码便是:
class Routing {
String current, previous, removed
dynamic args;
Route<dynamic>? route;
bool? isBack, isBottomSheet, isDialog;
经过运用这些成员你就能知道跳转的上一级, 当前页, 是否是按了back之类的.
补白1: 当你dismiss dialog时, isBack为true哦
补白2: 当isBack
= true时, previous
参数不太准, 所以不要过火信任这个previous
参数. 这个应该是Getx的一个bug.
2.3.2 带生命周期的中间件
你只需让一个或多个类继承自GetxMiddleware
, 重载它里边的某些生命周期办法就能做到阻拦/监听路由跳转了.
GetxMiddleware有多个办法, 总结如下:
图示:
1). 灰色背景的(前缀为M-
)便是Middleware中的生命周期办法
2). 粉色背景的(前缀为W-
)便是Widget自己的办法, 如build()
办法.
说一些我个人的运用经验, 那便是我会依靠大局binding (后边第三大节会讲到DI中的binding, 你能够理解为Koin或Dagger中的module), 而不是页面级别的binding. 所以这些办法里的onBindingStart
与onPageBuildStart
就比较少用.
用得较多时是在Widget创立之前做些处理, 即相似这样的:
if(isVip) goTo(vipPage)
else if(isGuest) goTo(loginPage)
...
那这时咱们就能够用 1. 注册中间件
GetMaterialApp(
GetPage(
name: "biz/cart",
page: () => CartPage(),
middlewares: [
AuthenticatedGate(), LogGate(),
],
2. 中间件中阻拦跳转
class AuthenticatedGate extends GetMiddleware {
@override RouteSettings redirect(String route) {
final authService = Get.find<AuthService>();
return authService.authed.value ? null : RouteSettings(name: '/login')
}
}
三. 依靠注入 (DI)
Getx的DI主要便是 Get.put(obj)
, 然后取出来用obj = Get.find()
. 这样就能存目标, 取目标了.
初看如同很简略, 但其实是有坑的, 特别是在运用Binding时.
3.1 Binding
现在假定咱们有两个页面, 一个是Home页展现各种产品, 另一个Detail页展现一种产品的概况. 在Getx中咱们能够运用Binding, 这个Binding相似Dagger中的module, 或是Koin中的module, 即供给目标的.
class HomeBinding implements Bindings {
@override
void dependencies() {
Get.lazyPut<HomeController>(() => HomeController());
Get.put<Service>(()=> Api());
}
}
class DetailsBinding implements Bindings {
@override
void dependencies() {
Get.lazyPut<DetailsController>(() => DetailsController());
}
}
这样你能够在路由体系中参加binding, 这样跳入到home页与detail页时就能自带上面的HomeController, Service, DetailsController这些目标了.
Get.to(Home(), binding: HomeBinding());
//或是:
Get.to(DetailsView(), binding: DetailsBinding())
3.2 Binding的缺陷
上面的写法, 其实有两个很大的缺陷.
第一个缺陷
Get.to(widgetObj, bindings)
是能够注入binding.
可是Get.toNamed()
并不支持binding参数啊. 我的跳转一般都是用toNamed的, 所以注定了这种办法我用不了.
第二个缺陷
这个缺陷很躲藏, 很容易出问题. 以上面的binding为例
-
HomeBinding
中供给了 HomeController, Service 两个目标 -
DetailsBinding
中供给了 DetailsController 目标 但其实咱们的Details页中也会用到Service目标.
之所以不呈现”details页中说找不到Service”的crash, 是由于用户先翻开的home页, Home已经往Get中写入了Service目标了, 所以等之后翻开detail页时, serivce目标已经有了, 能够Get.find()
得到, 所以不会有NPE过错.
但要是deep link的场景呢?
: 你直接跳到了Detail页, 结果就由于没有经过home页, 所以Service service = Get.find()
找不到service目标, 应用会crash.
所以现在就理解了, 第二个缺陷便是: 上面两个Binding有躲藏的依靠性 DetailsBinding其实依靠于HomeBinding. HomeBinding不先放好service, 那DetailsBinding供给不了Serivce, 就可能会让Detail页crash.
3.3 大局Binding
也便是像Dagger或Koin相同, 一开端就界说好一个大局的”目标供给表”, 即Dagger与Koin中讲的Module
啦.
这样优点是:
1). 由于是大局的, 所以咱们运用Get.toNamed("...")
也能运用到大局供给的目标
2). 由于是大局的, 所以没有什么Binding1依靠于Binding2的问题. 总共就一个Binding嘛, 天然没什么依靠的前后关系问题.
界说大局Binding
GetMaterialApp(
initialBinding: AppDiModule(), // 便是界说这个
...
);
class AppDiModule implements Bindings {
@override
void dependencies() {
Get.lazyPut(() => AppleRepository(), fenix: true);
Get.lazyPut(() => BoxService(), fenix: true);
Get.lazyPut(() {
BoxService service = Get.find();
return BoxRepository(service);
}, fenix: true);
}
}
具体事务页面中取出值
这个就容易了, 直接运用Get.find()
就行了. 如:
class DiPage2 extends StatelessWidget {
@override Widget build(BuildContext context) {
HoeService service = Get.find();
class DiPage3 extends StatelessWidget {
@override Widget build(BuildContext context) {
HoeRepository repo = Get.find();
final service = repo.service;
3.4 坑3: put(Clazz())多次是什么结果
若是咱们调用 Get.put(MyController())
三次, 之后再final ctrl = Get.find()
, 那这个ctrl是最新的ctrl(即后put的覆盖了前面的值), 仍是最老的ctrl(即后put的被疏忽了) ?
经过检查源码, 发现put时, 其实是放到了Map<String, dynamic>的变量池里了.
而这个map的key就相当于 obj.class + tag
.
一起留意, 若key相同, 那就主动疏忽, 不走map.put(key, value).
所以上面的问题的答案, 便是: find出来的是最早放入的ctrl. 后续put的值会被疏忽.
3.5 坑4: 同享controller
你只需前面Get.put(ctrl)后, 在其它页面中都能够用Get.find()来得到ctrl变量. 并且这个ctrl变量是相似于单例的, 即多个页面运用的ctrl是同一个目标. 这就相似Android中多个Fragment共用一个ViewModel
补白: Java中打印目标会有内存地址打印出来, 这样咱们就知道是不是同一个目标
而Dart中不会公开内存地址, 要想知道是不是同一个目标, 就得用obj.hashcode
. 只需hashcode相同, 那便是同一个目标
不过实践经验告诉我, 多个页面的GetxController仍是不要同享的好. 由于页1与页2共用了同一个ctrl时, 这样就相当于页2依靠了页1中的一些状态, 也就有了躲藏的依靠关系. 这是很容易出问题的.
这时要是想不同享, 那就得用tag
final ctrlOfPage1 = Get.put(MyController(), tag: "home")
final ctrlOfPage2 = Get.put(MyController(), tag: "detail")
//取出值
MyController ctrlOfPage1 = Get.find(tag: "home")
MyController ctrlOfPage1 = Get.find(tag: "home")
print("ctr1 = ${ctrolOfPage1.hashcode}, ctrl2 = ${ctrlOfPage2.hashcode}") //能够看出两个hashcode不相同, 即表明这是两个目标.
四. state办理
好了, 这个是我喜爱Getx的一点. 不过在先说之前, 先得说我最讨厌React Native的一点
4.1 声明式UI结构的一个遍及问题
像Flutter, React Native这些声明式UI结构, 我碰到过的功能问题, 主要是两点
1). 这些声明式UI满是UI结构, 一涉及到非UI的东西, 就要走Android, iOS端. 这时的来回传数据, 可能会有功能问题.
— 当然, 这个不肯定. 由于我在做一个React Native项目时, 我用crypto-js去解密一个著作时, 很慢. 但当我下沉到Android, iOS端去解密, 反而功能大大提高了. 所以仍是要看运用场景.
2). setState()
式的改写
也便是说你一个TextView要改写了, 结果咱们调用setState却是改写整个页面. 这一点在React Native里愈加明显, 特别是超长的list列表时, 功能超级差.
题外话: 由于这个setState的原因, 相对于React, 我其实更喜爱Solid JS, 这个SolidJS会知道你要改写哪个组件, 而去准确改写某一组件, 而不是改写整个页面, 但效率天然更快了
4.2 Getx对功能的提高
Getx就像Solid JS相同, 能准确改写某一个需求改写的组件, 而不是改写整个页面, 所以你的Flutter app效率天然就更好了.
补白: 由于不需求改写整个页面, 所以在运用Getx时, 完全能够不必StatefulWidget. 只是运用StatelessWidget根本上就够了.
根本运用办法
// 两种声明办法
final name = "".obs; //声明办法1, 运用obs
Rxn<ui.Image> imgSrc = Rxn<ui.Image>(); //声明办法2, 运用Rx或Rxn.
// 运用:
Obx( ()=> MyWidget(imgSrc.value) )
// 更新
name.trigger(newValue)
补白: Rxn
能够不供给初始值, 这在一些异步场景中就比Rx
更好用了.
4.3 Getx的state办理的先进性
讲过最大的优点之后, 咱们现在来全面地看一下Getx的statet办理的各种优点.
现在的flutter一些state办理库, 要么像Bloc相同运用很杂乱, 要么像Mobx相同要用代码生成(这很慢), 所以Getx想要快一点, 运用更简略的state办理.
-
说它简略, 是由于你不要运用什么StreamController, StreamBuilder, 或为一个状态专门创立一个类. 直接用
"name".obs
中的obs就能创立一个可监听的值- 并且你也不必像React相同, 自己界说
memo( oldProps, newProps => ...)
来自己界说要怎么比较. Getx会自己比较Obx(()=>widget)
中的值的前后改变, 来决定是否要更新的.
- 并且你也不必像React相同, 自己界说
-
说它快, 是由于它不必代码生成. flutter中的codegen真的很慢
-
别的, 最大的一个特点便是, 用了Getx的state办理之后, 你再也用不着StatefulWidget了. 只是StatelessWidget就够你用了! 功能天然也提高许多!
4.4 GetxController
这个就有点相似ViewModel, Presenter或Controller类. 你的那些数据以及操作数据的办法都在这儿, 如:
class MyResearchCtrl extends GetxController {
Rx<Color> color = Rx(Colors.indigo);
void setColor(Color c) => color.trigger(c);
}
这样你就能在多个页面中运用, 乃至是同享这个GetxController:
// 同享
class ControllerAndPage3 extends StatelessWidget {
@override Widget build(BuildContext context) {
final ctrl = Get.put(MyResearchCtrl());
class ControllerAndPage2 extends StatelessWidget {
@override Widget build(BuildContext context) {
final ctrl = Get.find<MyResearchCtrl>();
return Column(
children: [
Obx( () => Text(ctrl.color.toString)),
TextButton( onPressed: () {
ctrl.setColor(Colors.orange);
}, child),
]
)
4.5 生命周期
上面讲过Getx多是运用StatelessWidget. 但麻烦就来了, 即StatelessWidget没有生命周期办法
而StatefulWidget是有生命周期办法的, 其实是它的State有啦. 它的initState
与dispose
办法便是生命周期的开端与完毕), 那碰到这种要在页面翻开时注册某一东西, 然后在页面退出时注销掉什么(以免内存走漏)时, 要怎么办?
: 不必忧虑, GetxController就有生命周期, 这样就能替代StatefulWidget的生命周期.
4.6 题外话: 动画
上面讲了Getx根本上运用StatelessWidget就够了. 但有一种场景, 便是做动画. 咱们做动画是需求ticker的, 而一般用的ticker都是和State相匹配的Ticker.
class _MyState extends State<MynPage> with TickerProviderStateMixin {
这时我仅用StatelessWidget也能做动画吗?
: 答案是: 能够的, 只不过要借助GetxController的帮助.
Getx为了让咱们在StatelessWidget上也做动画, 供给了一个ticker, 叫GetSingleTickerProviderStateMixin
. 它是要求在GetxController上运用的, 所以咱们一般能够这样:
class MyAnimationPresenter extends GetxController with GetSingleTickerProviderStateMixin {
final int durationInMs;
late AnimationController animCtrl;
MyAnimationPresenter({required this.durationInMs}) {
animCtrl = AnimationController(vsync: this, duration: Duration(milliseconds: durationInMs));
}
结语
Getx供给了
- 更强壮的Router体系
- 愈加提高功能的State办理
- 更多Utils办法 (如求屏幕宽高, 如不必context就能显现dialog, …) 所以真的推荐运用Getx来架构你的app.
当然Getx还有DI体系, 只不过感觉还行, 只不过不是这么冷艳.
补白: 依照我个人喜爱, 其实我更喜爱用flutter-koin. 由于它简略, 好用, 还和我曾经在Android中运用的koin一脉相承. 不过Getx的DI也还行, 只不过没有Koin中的factory
, single
这样好用罢了.
架构
具体到体系架构上, 总结下便是:
- 把页面分解为 StatelessWidget 与 GetxController. 前者放UI, 后者放数据与事务逻辑
- 这一点相似于Android中的MVP, MVVM分层
- 在UI层面, 尽量运用StatelessWidget 与 Obx, 这样你的某一个数据改变时就只需去改写一个widget, 而不是setState式的改写整个页面. 这对咱们app的功能有帮助
- 全体app的共用依靠目标, 全都放到GetMaterialApp里的initialBinding里去. 这样一切的widget, controller等在需求时都能取到这些目标.
- 使用Router体系来便利咱们的跳转