flutter 路由2.0 的文章不少,大都是讲理论居多,本文主要讲实战。目前来说实际开发中很少需要兼容 web,说了那么多有关 url 的解析有什么用?不考虑 web,就省了很多事。
在 Navigator 2.0 中,如果不考虑 web,只需要实现 RouterDelegate 就可以了。每次都实现一遍太没效率。我们可以从 RouterDelegate17 开始。RouterDelegate17 是 RouterDelegate 的子类,实现了页面的跳转,弹出,替换等功能,还能监控页面的状态。
RouterDelegate17 的使用
完整示例代码点 这里。
示例演示
注意看第一行是页面状态,当弹出对话框的时候,下面的页面状态变为 PageStatus.Leave,对话框关闭的时候,页面的状态变为 PageStatus.enter。
- 安装
flutter pub add router_delegate17 复制代码
- 增加两个页面 PageA,PageB。为了方便使用,增加一个全局变量
final routerDelegate = RouterDelegate17([const MaterialPage(child: PageA())]);
- 使用
routerDelegate
MaterialApp( title: 'flutter RouteDelegate17 demo', home: Router( routerDelegate: routerDelegate, backButtonDispatcher: RootBackButtonDispatcher()), ); 复制代码
- 在 PageA 中 跳转 PageB
routeDelegate.push(MaterialPage(PageB()); 复制代码
- 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()); }), 复制代码
- 退出页面很容易
pop 可以带返回值
routerDelegate.pop(1) 复制代码
也可以直接 用
Navigator.of(context).pop(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 的话,需要注意一些问题。
- Navigator 的 key 要这样写
final _navigatorKey = GlobalKey<NavigatorState>(); @override GlobalKey<NavigatorState>? get navigatorKey => _navigatorKey; 复制代码
而不是
@override GlobalKey<NavigatorState>? get navigatorKey => GlobalKey<NavigatorState>(); 复制代码
我看到有人这样写了,所以解释一下。如果这样写,会导致 所有的 wiget 每次都重建,导致严重的性能问题。
完整示例代码点 [这里]