Flutter的Navigator2.0
Navigator1.0的不足
在介绍Navigator2.0之前, 我们先来说一下为什么需要Navigator2.0. Flutter团队在文章中给出了下面三个Navigator1.0的缺陷:
initialRoute参数, 即应用的初始页面, 在应用启动后就不能更改了. 如果在用户在应用中改变了设置, 使得应用应该更改初始页面, Navigator1.0没有很好的方法来解决.
Navigator1.0只为用户提供了高度封装的pop()、push()这样的api, 用户无法直接参与路由栈的管理. 而Flutter希望用户能够通过具有Flutter特色的销毁旧组件和重建新组件的方式来管理路由.
嵌套路由下, 系统的回退按钮只能由跟路由响应. 比如说应用拥有一个子路由栈, 用户在这个子路由栈中进行了一系列的操作, 然后点击系统的回退按钮, 这时回退事件是被根路由响应的, 结果常常是退出app或者改变整个根路由页面——这显然不是我们希望的.
我们思考一下Flutter团队给出的Navigator1.0的三个不足, 很容易就会发现, 它们出现的原因是相同的: Navigator1.0的封装级别太高, 导致用户无法插手路由的管理, 只能通过一系列高度封装的api来更新路由.
基于这一个观点, 我们可以想到, Navigator2.0一定是为我们提供了直接管理路由栈的某种媒介(路由栈的管理是框架层的实现, 自然不可能直接将其暴露出来供我们修改). 我们可以向通过BuildContext操作框架层的Element树一样, 通过这个媒介来管理框架层的路由栈.
Navitator2.0的使用
Navigator2.0提供了两种管理路由的方式, 一种是改良后的Navigator方式, 一种是新的Router方式.
Navigator方式
Navigator方式在Navigator2.0中最重大的一个改良就是引入了Page类.
Page的作用
顾名思义, Page这个类就是用来描述一个页面的. 我们通过一段代码比较来感受一下Page的作用:
在Navigator1.0中, 我们没有Page这个类, 于是注册页面的代码往往是这样的:
1 | class MyApp extends StatelessWidget { |
而拥有了Page之后, Navigator2.0注册页面的代码是这样的:
1 | class MyApp extends StatefulWidget { |
比较两段代码, 我们可以看到, **我们传统意义上写的基于StatelessWidget或者StatefulWidget的Page, 实际上只是这个Page的内容, 是一个Widget. 而Navigator2.0的Page是Widget的上一级, 它描述的是这个Page关于路由的信息. **如果要打个比方的话, 就类似于TCP/UDP和IP的关系一样. Widget就好像TCP/UDP协议, Page就好像IP报文头, 一个完整的页面就好像一个IP报文. 平常我们只使用上层的TCP/UDP协议, 就把封装好的IP报文头忽略了, 久而久之就认为IP好像就是TCP/UDP了. 其实TCP/UDP只是IP报文的负载, 真正保证IP报文正确传输的恰恰就是IP报文头.
Page的使用
我们再看上面两段代码, 还可以发现两个很有意思的点: 第一个是Navigator1.0中注册页面实际上注册的是一个Route, 而Navigator2.0中注册页面确实注册了一个Page; 第二个是App从StatelessWidget变成了StatefulWidget.
我们就从这两点展开来说一下Page怎么使用的.
首先是第一条, Navigator2.0中注册的是Page而不是Route. 我们可以很轻松地想到, Route作为框架体系组成之一, 是不可能轻易被废除的, 那么只能是Page又生成了Route.
1 | abstract class Page<T> extends RouteSettings { |
通过查看Page的源码, 我们可以看到刚才的猜想是正确的. 显然, 一个Page对应一个Route, 那么Page就是我们在一开始提到的, Flutter在Navigator2.0中为我们提供的操作路由栈的媒介. 那么具体是怎么操作呢?
刚才我们提到第二个有意思的点, App变成了StatefulWidget. 没错, 万能的setState来了. 既然一个Page对应一个Route, 那么我们只要往Pages里面加一个新的Page或者移除一个已有的Page, 然后再setState一下, Flutter自然就会更新路由栈了. 所以, Navigator2.0的AppState应该是下面这样的:
1 | class _MyAppState extends State<MyApp> { |
在这段代码中, 我们封装的push()函数非常简单, 主要看一下我们实现的_onPop函数. onPopPage是Navigator在Navigator2.0中新增的属性, 顾名思义, 这个属性就是接收到了回退事件后的回调函数. 这个函数返回true(也即route.didPop(result)函数的默认返回值)时, 系统路由栈会弹出栈顶的页面, 返回false时则不会, 所以我们可以根据这个函数来决定是否弹出页面, 这也是Navigator2.0中用户可以直接操作路由栈的体现之一. **需要注意的是, 当我们决定弹出时, 一定要更新Pages, 即也把Pages的最后一个Page(也就是栈顶的Page)移除, 否则Flutter在弹出栈顶Page后发现栈内页面与Navigator的pages不符合, 还会根据Navigator的pages重新生成路由, 这样pop相当于无效了. **
Router方法
Router是Navigator2.0的另外一个主要的控件, 提供了与Page+Navigator不同的一套路由管理方法.
1 | const Router({ |
上面是Router的构造函数, 除了Key外, 它有四个成员属性, 其中routeInformationParser和routerDelegate是使用Router方法中必须的两个组件(虽然routeInformationParser在Router里没有required, 但是在MaterialApp.router()方法和CupertinoApp.router()方法里是必须写的). 下面我们分别介绍一下这些组件.
RouteInformation
在介绍RouteInformationProvider和RouteInformationParser之前, 我们当然要了解一下RouteInformation这个概念.
1 | /// A piece of routing information. |
上面是RouteInformation的源码和注释. 注释是这么解释RouteInformation的: 它用来描述应用当前的路由位置, 是RouteInformationProvider和Router通信的媒介. 当路由位置发生变化时, RouteInformationProvider会将新的RouteInformation告知Route, 使其重新构建; 当路由栈变化时, Router会将新的RouteInformation告知RouteInformationProvider, 使其同步给Flutter的宿主.
听起来很抽象对吧? 那么我们用大白话翻译一下:
RouteInformation是一个页面的Url, RouteInformationProvider与Flutter的宿主(在这个例子里是浏览器)同步页面Url, Route负责管理路由栈. 当用户在浏览器的Url栏里输入新的Url后, 浏览器会自动跳转的新Url对应的页面; 当浏览器跳转到新的页面(比如用户点击了浏览器的回退键), 浏览器的Url栏也会更新成新页面的Url.
RouteInformationProvider
1 | abstract class RouteInformationProvider extends ValueListenable<RouteInformation?> { |
上面是RouteInformationProvider的源码, 可以看到, 这个类非常简单, 就只有一个routerReportsNewRouteInformation方法. Router通过调用这个方法来通知Flutter的宿主, 路由发生了变化, 需要更新应用的路由位置. 这个类有一个默认的实现类PlatformRouteInformationProvider, Flutter就是通过它与宿主通信更新路由位置的.
RouteDelegate
RouteDelegate是Router方法中的核心, 实际上Router这个类只是一个外壳, 真正发挥作用的只是RouteDelegate, 其它的成员属性都是RouteDelegate的一个补充. 它扮演着AppState在Navigator方法中的角色——管理路由状态. 事实上, RouteDelegate的实现代码和Navigator方法中AppState的实现方式是很相似的:
1 | class MyApp extends StatelessWidget { |
可以看到, 使用RouteDelegate的代码和前面使用AppState的方法几乎一模一样, 只是把setState改成了notifyListeners. 如果我们查看RouteDelegate的源码, 我们会发现它混入了ChangeNotifier接口(其实RouteDelegate本身也和ChangeNotifier一样继承自Listenable, 混入Listenable只是为了不再重新实现一遍接口), 显然它是被监听的. 至于是被谁监听, 通过查找引用我们会发现, 是被Router的State——RouterState监听的, 当我们调用ChangeNotifier后, RouterState会执行setState来重新构建路由栈.
这里顺便再说一下混入的另外一个接口: PopNavigatorRouterDelegateMixin.
1 | mixin PopNavigatorRouterDelegateMixin<T> on RouterDelegate<T> { |
我们看到这个类也继承了RouterDelegate, 并实现了popRoute方法. popRoute方法是响应系统回退的回调, 这个类实现popRoute方法的逻辑是, 通过Navigator的maybePop来保证路由在没有页面的情况下不会继续pop. 如果我们想自己实现这个方法, 就可以不混入这个类.
RouteInformationParser
从上面的RouteDelegate使用实例中我们可以看到, RouteDelegate是接受泛型的, 也就是说, 我们重写的setRoutePath这些函数的参数configuration的类型是不一定的. 既然, RouteInformationProvider和Router通过RouteInformation通信, Router真正发挥作用的成员RouteDelegate使用的是一个泛型, 那么就肯定要有一个中间类把RouteInformation解析成RouteDelegate所使用的泛型, 这个类就是RouteInformationParser了.
1 | abstract class RouteInformationParser<T> { |
上面是RouteInformationParser的源码, 这个类也很简单, 就是两个函数. 一个将RouteInformation转成T, 一个将T转成RouteInformation. 显然前者是RouteInformationProvider同步消息给Router使用的, 后者是Router同步消息给RouteInformationProvider使用的. 值得一提的是, 第二个函数不是必须要重写的, 如果不重写的话, 页面回退时Url就不会同步变化了.
BackButtonDispatcher
BackButtonDispatcher解决的就是嵌套路由响应系统回退事件的问题. 当我们有两个以上的路由嵌套, 遇到系统回退事件时, 究竟谁应该响应呢? 这个情景和事件传递的情景很像, 自然解决方法也就和事件传递的解决方法很像了.
BackButtonDispatcher有两个子类: RootBackButtonDispatcher和ChildBackButtonDispatcher. 我们可以很快想到, 前者是这个事件传递树的根节点, 负责下发事件和最终拦截事件, 后者是这个事件传递树的中间节点和叶节点, 可以下发事件也可以提前拦截事件.
1 | abstract class BackButtonDispatcher extends _CallbackHookProvider<Future<bool>> { |
上面是BackButtonDispatcher的源码, 其主要逻辑就是invokeCallBack方法, 这个函数也简单易懂, 就是先问子节点是否处理事件, 如果子节点不处理那么自己就处理(默认是自己不处理, 返回defaultValue=false, 如果自己想拦截就返回true). 有一个和事件分发机制不同的细节是, 这里询问子节点是倒序遍历的, 因为新的子节点是在list的后面的, 所以要从后往前询问.
源码里其它的函数基本上都是更改优先级的, forget是不让某个子节点处理事件, deferTo是把这个子节点的优先级提到最高, takePriority是不询问子节点, 自己直接处理.
至于两个子类的源码, 因为都很简单, 一个是绑定WidgetsBinding监听, 一个是管理子节点, 这里就不再介绍了.