使用 Flutter Navigator2.0 最舒服的姿势

时间:2021-04-12 01:26:15

大家好,我是 17。

flutter 路由2.0 的文章不少,大都是讲理论居多,本文主要讲实战。目前来说实际开发中很少需要兼容 web,不考虑 web,就省了很多事。

在 Navigator 2.0 中,如果不考虑 web,只需要实现 RouterDelegate 就可以了。每次都实现一遍太没效率。我们可以从 RouterDelegate17 开始。RouterDelegate17 是 RouterDelegate 的子类,实现了页面的跳转,弹出,替换等功能,还能监控页面的状态。

RouterDelegate17 的使用

完整示例代码点 这里。

示例演示

使用 Flutter Navigator2.0 最舒服的姿势

注意看第一行是页面状态,当弹出对话框的时候,下面的页面状态变为 PageStatus.Leave,对话框关闭的时候,页面的状态变为 PageStatus.enter。

  1. 安装
flutter pub add router_delegate17
  1. 增加两个页面 PageA,PageB。为了方便使用,增加一个全局变量 final routerDelegate = RouterDelegate17([const MaterialPage(child: PageA())]);
  2. 使用 routerDelegate
MaterialApp(
   title: 'flutter RouteDelegate17 demo',
      home: Router(
          routerDelegate: routerDelegate,
          backButtonDispatcher: RootBackButtonDispatcher()),
   );
  1. 在 PageA 中 跳转 PageB
routeDelegate.push(MaterialPage(PageB());
  1. PageB 中打开对话框
routeDelegate.openDialog ...其它省略,详见完整代码

openDialog 就是系统 showDialog 的 包装,参数都一模一样,包装的目的,是为了可以让页面能更新状态。

看到这里,你会发现,routeDelegate17 实现的方法和 navigator1.0 的 方法很象,这样会降低使用成本。

所有的页面都在栈中。页面状态是 RouterDelegate17 实现的功能。RouterDelegate17 把页面分为三个状态

  • PageStatus.none 新页面刚入栈的时候
  • PageStatus.leave 顶层页面被其它页面遮挡的时候
  • Pagestatus.enter 被遮挡的页面重新成为顶层页面的时候

要获取页面状态非常容易,只需要获取 status 属性

var pageStatus = routerDelegate.currentNavSettings.status.value

status 是一个 ValueNotifier,实际用的时候可以用 ValueListenableBuilder 监听变化

ValueListenableBuilder(
     valueListenable: routerDelegate.currentNavSettings.status,
     builder: (context, value, child) {
       return StatusText(text: value.toString());
 }),
  1. 退出页面很容易

pop 可以带返回值

routerDelegate.pop(1)

也可以直接 用

Navigator.of(context).pop(1)
  1. 退出程序请求计数

当在首页试图弹出页面的时候,android 系统默认行为会退出程序。routeDelegate17 会报告退出程序的次数,由调用方决定如何处理

routerDelegate.exitCount.addListener(() {
      setState(() {
        exitCount = routerDelegate.exitCount.value;
        //实际应用中这里给出警告。 2 秒后 exitCount 恢复为 0
        if (exitCount == 1) {
          countText = '在首页按 back 键 $exitCount 次';
        }
        //实际应用中这里执行退出程序操作。 2 秒后 exitCount 恢复为 1
        if (exitCount == 2) {
          countText = '在首页按 back 键 $exitCount 次';
        }
        //2 秒内如果一直按会一直增加。
        else{
          countText = '在首页按 back 键 $exitCount 次';
        }
      });
 });

exitCount 是 ExitCount 的实例. 和普通的计次不同,每次增加次数后, delay 时间后都会被减掉。 应用程序可以监听 value 的变化来决定是否要退出程序。

在使用方面就讲完了。下面开始闲聊。

RouterDelegate17 闲聊

随意转到任意页面

你可能会觉得,这是在用 Navigator2.0 吗?怎么感觉和 Navigator1.0 一样啊。就是要达成这样的效果。用已有的习惯和用法能解决问题,为什么要新造一套?除了增加使用成本,没什么好处。虽然 push,pop,replace 在外表上看是一样的,但实际能力还是有增强的。假设有依次 push 三个页面 A,B,C,想从 c 回来A,用 1.0 api要么再push 一个 A,要么 pop C,pop B ,都不是想要的解决办法。用 RouterDelegate17 可以直接 push(A),也就是说,Navigator2.0 可以直接方便的跳到任何页面(新页面或栈中的任何页面),没有任何副作用。

初始化路由栈的能力

在 Navigator1.0 中 用 [navigator.initialRoute] (api.flutter.dev/flutter/wid…) 只能设置一个初始路由。但是在有的情况下,需要初始化一个路由栈。比如一个应用有两个页面,首页和详情页面。通过deeplink 直接打开详情页面,这时就应该初始化路由栈 [首页,详情页面]。为什么要首页也要初始化?是为了和正常打开app时形成一样的路由栈。这一点很重要,否则无法统一处理跳转逻辑。RouterDelegate17.setInitialPages 可以方便的设置初始路由栈。

简化页面状态监听

本来呢,页面状态是可以用 Observer + RouteAware 的。但是呢,实现了 RouteAware 的方法还是不能直接用,因为最终还是得体现在页面中,可能还得 setState,或 用 ValueListenableBuilder 改变页面状态。RouterDelegate17 一步到位,直接给出 status ,status 本身就是一个 ValueListenable,可以直接用。使用也非常简单,直接一句代码就可以引用。(前文有讲)

方不方便还是主要原因,毕竟如果能用还是可以用的。但是如果用 RouteAware ,在获取当前路由的时候需要用 ModalRoute.of(context) 拿到当前路由。最终是通过 inheritWidget 的方式拿到路由。inheritWidget 有一个更新机制,当判断函数为真的时候,会进行刷新

bool updateShouldNotify(_ModalScopeStatus old) {
    return isCurrent != old.isCurrent ||
           canPop != old.canPop ||
           route != old.route;
}

这个判断有时会导致不必要的刷新,而且 updateShouldNotify 是没法 override 的。 RouterDelegate17 没有这个烦恼,不用你自己去拿当前路由,自动管理了。

最后还有一个原因,RouteAware 对弹出 dialog 这种是不会监听的。没有监听就不能用和 page 一样的方式来处理页面状态改变。RouterDelegate17 把 dialog 和 page 统一处理。比如 在 PageA 中打开对话框,PageA 的状态变为 PageStatus.leave。在 PageA 中 打开新页面 PageB,PageA 的状态也变为 PageStatus.leave。

为什么没有用状态驱动的方式

除了用这种 api 的方式,还有一种是状态状态驱动的方式。就是把 app 中与路由相关状态都拿出来做为状态来驱动路由的变更。这种感觉上很高级的样子,但实操上会丧失很多灵活性,并且让逻辑更加复杂。就拿 web 开发来说吧。无论是 React 还 是 Vue,最后还是落到 push 等方法上,而不是用状态驱动的方式。

其它

还有退出程序这种小福利就不讲了。知道方便就行了。

使用 Navigator 2.0 注意事项

我觉得目前 RouterDelegate17 是最舒服的使用 Navigator 2.0 的姿势了,可能你还觉得不够,如果你要自己实现或修改 RouterDelegate17 的话,需要注意一些问题。

  1. Navigator 的 key 要这样写
final _navigatorKey = GlobalKey<NavigatorState>();
  @override
  GlobalKey<NavigatorState>? get navigatorKey => _navigatorKey;

而不是

@override
 GlobalKey<NavigatorState>? get navigatorKey => GlobalKey<NavigatorState>();

我看到有人这样写了,所以解释一下。如果这样写,会导致 所有的 wiget 每次都重建,导致严重的性能问题。

完整示例代码点 [这里]


2022 年 2.7 日补充

今天收到有人给这篇文章点赞,于是打开又回顾了一下。这是很早之前写的一篇文章。RouterDelegate17 的实用价值很大,因为导航可以影响到整个项目。 别看文章的赞不多,模块的 start 也没有,但真的很好用,如果你能耐心的把代码 RouterDelegate17 的代码看完(很短),可能就会赞同我的想法。可能有人担心将来支持 web 怎么办,到时进行升级就行了,只要接口不变,对使用者是无感的。只是现在没有发布 web 的需求,如果要支持 web 要多写好多代码,复杂度也会增加很多,所以暂时没有实现罢了。

感谢给这篇文章点赞的小伙伴!