likes
comments
collection
share

三分钟让Flutter路由实现SingleTask启动模式

作者站长头像
站长
· 阅读数 55

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 来实现的。

三分钟让Flutter路由实现SingleTask启动模式

其实我们看 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(),
    ),

    ...

三分钟让Flutter路由实现SingleTask启动模式

我们在登录页面返回到首页,看似没有初始化 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 实现的话有什么方案?

  1. 拦截 Get 的全部路由方案,启动的时候添加到自己的路由栈中,关闭的时候移除?

不靠谱!因为如果是返回的话,例如 until 是可以使用表达式返回多个页面的,就无法准确的记录路由表数据。

  1. 重写 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);
    }
  }

  @override
  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);

效果:

三分钟让Flutter路由实现SingleTask启动模式

Get.offNamed(RouterPath.AUTH_SIGNUP)

三分钟让Flutter路由实现SingleTask启动模式

Get.back()

三分钟让Flutter路由实现SingleTask启动模式

多次页面返回 Get.until((route) => route.settings.name == RouterPath.MAIN);

三分钟让Flutter路由实现SingleTask启动模式

三分钟让Flutter路由实现SingleTask启动模式

貌似没什么问题?那我们就可以拿到目标页面是否存在栈中,就能解决是前进跳转还是后退跳转,也就能实现 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),基本上囊括了前进跳转也后退跳转的场景:

三分钟让Flutter路由实现SingleTask启动模式

三分钟让Flutter路由实现SingleTask启动模式

后记

本文是从发现问题,到解决问题,期间踩过的坑与最终实现的思路记录。

最终的实现思路还是参考 Android 开发的思路,自己实现路由栈的管理,由于我的功能比较简单,并没有在栈中做跳转,清除栈等操作。

这般实现还有一个好处就是并不局限与 GetX 框架,支持原生的路由的。并且后续还能继续扩展,比如 SingleTop 的启动模式。例如目前在首页,按下 Home 键之后点击推送可以回到 Home 键,此时需要 SingleTop 的启动模式。

如何实现?我心里大概有思路了,但是我们没有做到这一步,目前没这个需求,后期有时间的话我会讲一下如何实现 SingleTop 的启动模式。

由于我不知道知道其他人是怎么实现的,可能我资质愚钝并没有在网上找到什么好的方案,所以自己硬着头皮简单的实现了一下。

诚惶诚恐!如果已经有好的实现方式,还请告知大家一起交流,如本文讲的错漏的地方,希望同学们可以评论区指出。

本文最终代码在文章末尾,有兴趣的可以复制代码进行试验,都是比较简单的工具代码,就没有封装库了,关于后续我也会持续分享一些实际开发中 Flutter 的踩坑与其他实现方案思路,有兴趣可以关注一下。

如果感觉本文对你有一点点点的启发,还望你能点赞支持一下,你的支持是我最大的动力啦。

Ok,这一期就此完结。

三分钟让Flutter路由实现SingleTask启动模式

转载自:https://juejin.cn/post/7281488026832158781
评论
请登录