Flutter 的 路由发动形式完成思路
前语
假设你是 Android 开发者,请放心食用,仿照 Android 的页面发动形式封装的,假设是其他端的开发者呢,能够略过剖析与思路直接文章结尾拿走代码即可。
工作是怎样一回事呢?有时分咱们想跳转到一个页面,不知道是用 to ,仍是用 until ,假设要跳转到指定页面并封闭之前的页面,也不知道用 off 仍是用 until 。
因为咱们只能写行进的跳转仍是撤退的跳转,他不支撑像 Android 那样的 SingleTop SingleTask 的发动方式。
这儿举例一个场景我们就理解了。
场景:假设我现在收到推送了,点击告诉栏需求跳转到 Flutter 的页面,咱们要根据业务逻辑挑选性的跳转到告诉页面或许主页。
那么怎样跳转?假设现已在告诉页面或许现已在主页了,又怎样跳转?假设在主页或许告诉页面的二级页面又该怎样跳转?
因为咱们不确定主页在不在,不确定告诉页面在不在,假设是写 Android 运用,那简略了,直接设置发动形式 SingleTask ,它就会主动把告诉页面之前的栈悉数清掉。
那么 GetX 或许说原生的 Navigator 能不能完成相似的功用呢?
一、GetX的路由跳转
咱们都知道,其实 GetX 的路由跳转也是基于 Navigator 的封装,本身并没有持有路由栈方针,实质上仍是 NavigatorState 内部持有路由栈。
关键的关键是 _RouteEntry 与 _history 路由栈都是私有的,咱们无法经过重写或扩展办法来拿到路由栈从而完成自定义功用。
可是咱们能够经过了解 NavigatorState 中几种原生跳转办法的原理,能够曲线救国完成相似 SingleTask 的功用。
这儿先从 GetX 的几种路由跳转办法介绍:
1. 直接跳转
//导航到新页面
Get.to(NextScreen()); //一个是自己new方针
Get.toNamed(RouterPath.NEXT); //一个是经过Name标识
这两个办法不用说,最基本的办法,咱们实践开发用 Get.toNamed(RouterPath.NEXT)
的方式更多一些。
实质上其实调用的是
Navigator.of(context).pushNamed(…)
作用便是不论3721敞开一个新页面,不论这个页面是否现已存在。
以咱们推送跳转的例子来说,假设咱们现在就在音讯告诉页面,点击推送告诉栏跳转到音讯告诉页面。那么此刻的作用便是创立了一个新的音讯告诉页面。
此刻回来上一级页面成果发现仍是音讯告诉页面,而且两个页面的内容作用是共同的,因为运用的是同一个Controller,同一个State。(假设想开同一个页面不同的作用需求运用tag,这又是另一个故事了,不多介绍)
2. 直接跳转并封闭当时
//进入下一个页面并取消之前的所有路由
Get.offAll(NextScreen()); //一个是自己new方针
Get.offAllNamed(RouterPath.NEXT); //一个是经过Name标识
关于 offAll 的这一组,其实便是封闭悉数的页面再跳转到指定的页面。
实质上和原生的这样写法没什么区别
Navigator.of(context) .pushNamedAndRemoveUntil(RouterPath.NEXT, (Route route) => false, arguments: arguments)
作用便是管你3721,先把你悉数页面封闭了再说,然后再帮你敞开一个新的页面。
以咱们推送跳转的例子来说,假设咱们现在从主页跳转到了音讯告诉页面,点击推送告诉栏跳转到音讯告诉页面。那么此刻的作用便是把主页和音讯告诉页面悉数封闭了,然后又从头敞开了一个新的音讯告诉页面,然后需求从头 Loading 加载数据。
然后回来音讯告诉页面之后直接退出运用
了,你敢信?这神仙操作!
3. 直接跳转并封闭多个指定条件路由
另外便是 offUntil 这一组:
Get.offUntil(NextScreen(), (route) => false);
Get.offNamedUntil(RouterPath.NEXT, (route) => route.settings.name == RouterPath.NEXT);
它其实是相似 offAll 那一组的,仅仅 offAll 是封闭悉数的页面,而 offUntil 这一组便是能够自定义表达式,内部能够写一些表达式,当条件回来 true 时,停止移除路由。
可是仍是会先增加再移除,比方咱们从主页跳转到登录页面, 当登录成功之后需求从登录页面回来主页。
Get.offNamedUntil(RouterPath.MAIN, (route) => route.settings.name == RouterPath.MAIN);
咱们假设用这一种办法,那么便是把 RouterPath.MAIN 理由之前的路由悉数铲除,理论上是能够达到 SingleTask 的作用,可是它确又增加了一个 MainPage,导致现在页面上有两个 MainPage 了。不符合咱们的预期。
那咱们用 offAll 呢?
Get.offAllNamed(RouterPath.MAIN);
的确是能跳转到新的主页了,可是它的逻辑是先铲除悉数的路由栈,然后再增加一个新的 MainPage ,这样就导致我的 MainPage 需求从头加载了,之前保存的状态都没了,无法承受!
至于直接用 toNamed 那肯定也是不行,因为也会又创立一个 MainPage 页面。
怎样办,还剩余几个Get路由看看再说。
4. 跳转页面并封闭当时页面
off 这一组和咱们 Android 的一些页面跳转相似 startWithPop 的逻辑。
Get.off(NextScreen()); //一个是自己new方针
Get.offNamed(RouterPath.NEXT); //一个是经过Name标识
实质上它是调用了原生 Navigator 的 pushReplacementNamed 。也便是把之前的页面替换掉,从而完成跳转并封闭页面的作用。
5. 回来
Get.back();
//相当于SetResult给上一个页面传递数据
Get.back(result: 'success');
Get.until((route) => route.settings.name == RouterPath.MAIN)
back实质是调用了 Navigator 的 pop 办法,这个就不多介绍了,回来页面能够挑选携带参数回来。
until实质是调用了 Navigator 的 popUntil 办法,就能够回来到指定的页面。
那么咱们回到咱们之前的主页与登录页面,登录成功之后从登录页面回来到主页该怎样写?
咱们能够运用
Get.back();
Get.until((route) => route.settings.name == RouterPath.MAIN)
都能够完成回来的功用,横竖咱们知道 MainPage 在吗,那的确是能够直接回来,可是假设 MainPage 不在呢?比方退出登录之后咱们把悉数的路由清掉了跳转到登录页面。
那么此刻你登录成功之后逻辑假设仍是 back 和 until 那岂不是登录成功直接退出运用?太傻了吧。
这其实是和文章开头推送跳转的逻辑是相同的了,我不知道我要跳转的页面在不在,所以我不知道要行进仍是回来。
那么怎样处理这个问题呢?难道只能用最傻的办法,悉数封闭再敞开页面?
二、GetX的自定义路由发动形式
2.1 不靠谱计划一
那么网上有没有什么好的处理计划了,我看了下也是引荐运用 Navigator 的 pushNamedAndRemoveUntil 来完成的。
其实咱们看 pushNamedAndRemoveUntil 终究的完成源码就能够得知,它是先把咱们的方针路由存入,然后在履行表达式封闭到指定的条件的页面。
比方咱们要从登录跳转到主页,那么便是敞开一个主页而且封闭到主页。
Get.offNamedUntil(RouterPath.MAIN, (route) => route.settings.name == RouterPath.MAIN);
此刻的成果便是一同存在两个主页。那么咱们这儿先 add 的路由是在栈顶的,咱们能够不能够直接 navigator.pop 直接把刚增加的路由出栈不就行了?
这骚操作也行?你别急,这还真行!
在主页存在的情况下的确能够比较完美的完成 SingleTask 的逻辑。当然这是在主页存在的情况下才能完成的。
我心想我要是知道主页存在了,我直接 Get.until((route) => route.settings.name == RouterPath.MAIN)
不香吗?
最后这种计划,它仍是不支撑主动的判别行进和撤退,假设是撤退的勉强能用,可是假设是行进的你加上back之后就无法正常运用了。
2.2 不靠谱计划二
突发奇想,咱们能不能让主页坚持单例,这样不就能够了?像 Activity 在清单文件中指定 SingleTop 相同。
修正代码如下:
class MainPage extends StatefulWidget {
static const MainPage _instance = MainPage._internal();
factory MainPage() {
return _instance;
}
const MainPage._internal();
static MainPage get instance => _instance;
@override
State<MainPage> createState() => _MainPageState();
}
在路由中,咱们一致供给咱们单例方针
...
GetPage(
name: RouterPath.MAIN,
page: () => MainPage.instance,
binding: MainBinding(),
),
...
咱们在登录页面回来到主页,看似没有初始化 MainPage,这仅仅因为他是单例了,可是仍是会两个MainPage。
天真!
这种计划实质上并没有修正什么,仅仅修正了单例页面,展现了同样的一个方针页面,仍是会有方针页面是否存在的判别问题。
2.3 不靠谱计划三
已然最后仍是要判别方针页面是否现已存在,咱们直接自定义一个办法露出不就行了吗,为了方便咱们能够运用扩展办法扩展 Get 与 NavigatorState 直接露出办法。
经过源码咱们发现,路由的跳转与之对应的入栈,出栈是由 Navigator 中的 NavigatorState 持有的。
history 方针,持有当时栈方针 RouteEntry 也便是咱们的路由实体。
那么咱们的判别办法就能够这么写:
extension GetRouterNavigation on GetInterface {
bool hasRouterByName({int? id, required String routerName}) {
return global(id).currentState?.hasRouterByName(routerName) ?? false;
}
}
extension RouterNavigator on NavigatorState {
bool hasRouterByName(String routerName) {
final Iterator<_RouteEntry> iterator = _history.where(_RouteEntry.isPresentPredicate).iterator;
if (!iterator.moveNext()) {
return false;
}
if (iterator.current.route.settings.name == routerName) {
return true;
}
if (!iterator.moveNext()) {
return false;
}
return false;
}
}
可是 history 和 RouteEntry 都是私有的,咱们就算用扩展办法也无法拜访。查看其他的核心履行办法都是私有办法,无法修正。
额,尽管完成不了,可是这个思路是对的,咱们查询当时的路由栈,查询方针路由是否现已存在,假设存在就运用撤退的跳转办法,假设不存在就运用行进的跳转办法。
2.4 靠谱计划四
已然 Navigator 不露出路由栈给咱们拜访与查询,那么咱们能不能自己完成一个路由栈?
基于 Get 完成的话有什么计划?
- 阻拦 Get 的悉数路由计划,发动的时分增加到自己的路由栈中,封闭的时分移除?
不靠谱!因为假设是回来的话,例如 until 是能够运用表达式回来多个页面的,就无法精确的记载路由表数据。
- 重写 GetController ?创立的时分增加到路由栈?毁掉的时分移除路由栈?
不靠谱,只说一点,有些 Controller 是几个页面或许不同的页面一同持有的,那么 Controller 就无法精确的记载路由表数据。
那怎样办?咦?咱们 Android 的页面栈,咱们不是运用的一个 ActivityManager 来办理页面栈的吗?
咱们 Activity 是在 Application 的 ActivityLifecycleCallbacks 监听中获取到 Activity 的创立与毁掉中进行增加栈与移除栈的操作吗?
那 Flutter 有没有相似的监听呢?咦?咱们在前文中不是有原生 Navigator 的监听中处理 GetX 的路由的兼容设置吗?
咱们在里面进行页面的增加栈与移除栈操作行不行呢?试试!
...
navigatorObservers: [GetXRouterObserver()],
...
具体的监听器完成:
class GetXRouterObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
RouterReportManager.reportCurrentRoute(route);
MyRouterHistoryManager().putRouterByName(route.settings.name);
Log.d('增加之后-当时的My路由表:${MyRouterHistoryManager().routeNames.toString()}');
}
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) async {
RouterReportManager.reportRouteDispose(route);
MyRouterHistoryManager().removeRouterByName(route.settings.name);
Log.d('Pop之后-当时的My路由表:${MyRouterHistoryManager().routeNames.toString()}');
}
@override
void didRemove(Route route, Route? previousRoute) {
MyRouterHistoryManager().removeRouterByName(route.settings.name);
Log.d('Remove之后-当时的My路由表:${MyRouterHistoryManager().routeNames.toString()}');
}
@override
void didReplace({Route? newRoute, Route? oldRoute}) {
MyRouterHistoryManager().putRouterByName(newRoute?.settings.name);
MyRouterHistoryManager().removeRouterByName(oldRoute?.settings.name);
Log.d('Replace之后-当时的My路由表:${MyRouterHistoryManager().routeNames.toString()}');
}
}
咱们需求监听各种的条件,因为 Navigator 有这么几种操作 push pop popUntil pushReplacementNamed 等操作,对应的便是上面的几种回调。
而 GetX 实质是调用的这几种 Navigator 办法,所以完全是可用的。
剩余的路由栈办理的单例类如下:
class MyRouterHistoryManager {
static final MyRouterHistoryManager _instance = MyRouterHistoryManager._internal();
factory MyRouterHistoryManager() {
return _instance;
}
MyRouterHistoryManager._internal();
final List<String?> _routeNames = [];
void putRouterByName(String? routeName) {
if (routeName != null) {
_routeNames.add(routeName);
}
}
void removeRouterByName(String? routeName) {
if (routeName != null && _routeNames.contains(routeName)) {
_routeNames.remove(routeName);
}
}
//获取到悉数的RouterName数组
List<String?> get routeNames => _routeNames;
//查询当时栈中是否存在指定的路由名称
bool isRouteExist(String routeName) {
return _routeNames.contains(routeName);
}
}
咱们能够实验一下各种情况
Get.offNamed(RouterPath.MAIN);
作用:
Get.offNamed(RouterPath.AUTH_SIGNUP)
Get.back()
屡次页面回来 Get.until((route) => route.settings.name == RouterPath.MAIN);
形似没什么问题?那咱们就能够拿到方针页面是否存在栈中,就能处理是行进跳转仍是撤退跳转,也就能完成 SingleTask 的逻辑啦。
剩余的就简略啦,咱们仿照 GetX 的路由跳转规则写一个 SingleTask 发动形式:
extension GetRouterNavigation on GetInterface {
/// 查询指定的RouterName是否存在自己的路由栈中
bool isRouteExist(String routerName) {
return MyRouterHistoryManager().isRouteExist(routerName);
}
/// 跳转页面SingleTask形式
void toNamedSingleTask(
String routerName, {
dynamic arguments,
void Function(dynamic value)? cb,
Map<String, String>? parameters,
}) {
if (isRouteExist(routerName)) {
Get.until((route) => route.settings.name == routerName);
} else {
Get.offNamed(routerName, arguments: arguments, parameters: parameters)?.then((value) => {
if (cb != null) {cb(value)}
});
}
}
}
这样不论是从主页跳转到登录页面(ToNamed),仍是从登录页面跳转到注册页面(toNamedSingleTask),仍是从注册页面跳转到主页(toNamedSingleTask),基本上包括了行进跳转也撤退跳转的场景:
跋文
本文是从发现问题,到处理问题,期间踩过的坑与终究完成的思路记载。
终究的完成思路仍是参考 Android 开发的思路,自己完成路由栈的办理,因为我的功用比较简略,并没有在栈中做跳转,铲除栈等操作。
这般完成还有一个优点便是并不限制与 GetX 框架,支撑原生的路由的。而且后续还能继续扩展,比方 SingleTop 的发动形式。例如现在在主页,按下 Home 键之后点击推送能够回到 Home 键,此刻需求 SingleTop 的发动形式。
怎样完成?我心里大概有思路了,可是咱们没有做到这一步,现在没这个需求,后期有时间的话我会讲一下怎样完成 SingleTop 的发动形式。
2023-09-26 更新:
更进一步,Flutter路由的SingleTop发动形式 & 冷热发动指定页面的完成
因为我不知道知道其他人是怎样完成的,或许我资质弛禁并没有在网上找到什么好的计划,所以自己硬着头皮简略的完成了一下。
诚惶诚恐!假设现已有好的完成方式,还请奉告我们一同沟通,如本文讲的讹夺的当地,期望同学们能够评论区指出。
本文终究代码在文章结尾,有爱好的能够复制代码进行实验,都是比较简略的东西代码,就没有封装库了,关于后续我也会继续共享一些实践开发中 Flutter 的踩坑与其他完成计划思路,有爱好能够重视一下。
假设感觉本文对你有一点点点的启发,还望你能点赞
支撑一下,你的支撑是我最大的动力啦。
Ok,这一期就此结束。