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);
    }
  }
  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 的发动形式。

2023-09-26 更新:

更进一步,Flutter路由的SingleTop发动形式 & 冷热发动指定页面的完成

因为我不知道知道其他人是怎样完成的,或许我资质弛禁并没有在网上找到什么好的计划,所以自己硬着头皮简略的完成了一下。

诚惶诚恐!假设现已有好的完成方式,还请奉告我们一同沟通,如本文讲的讹夺的当地,期望同学们能够评论区指出。

本文终究代码在文章结尾,有爱好的能够复制代码进行实验,都是比较简略的东西代码,就没有封装库了,关于后续我也会继续共享一些实践开发中 Flutter 的踩坑与其他完成计划思路,有爱好能够重视一下。

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

Ok,这一期就此结束。

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