Flutter的Navigator2.0

Navigator1.0的不足

在介绍Navigator2.0之前, 我们先来说一下为什么需要Navigator2.0. Flutter团队在文章中给出了下面三个Navigator1.0的缺陷:

  1. initialRoute参数, 即应用的初始页面, 在应用启动后就不能更改了. 如果在用户在应用中改变了设置, 使得应用应该更改初始页面, Navigator1.0没有很好的方法来解决.

  2. Navigator1.0只为用户提供了高度封装的pop()、push()这样的api, 用户无法直接参与路由栈的管理. 而Flutter希望用户能够通过具有Flutter特色的销毁旧组件和重建新组件的方式来管理路由.

  3. 嵌套路由下, 系统的回退按钮只能由跟路由响应. 比如说应用拥有一个子路由栈, 用户在这个子路由栈中进行了一系列的操作, 然后点击系统的回退按钮, 这时回退事件是被根路由响应的, 结果常常是退出app或者改变整个根路由页面——这显然不是我们希望的.

我们思考一下Flutter团队给出的Navigator1.0的三个不足, 很容易就会发现, 它们出现的原因是相同的: Navigator1.0的封装级别太高, 导致用户无法插手路由的管理, 只能通过一系列高度封装的api来更新路由.

基于这一个观点, 我们可以想到, Navigator2.0一定是为我们提供了直接管理路由栈的某种媒介(路由栈的管理是框架层的实现, 自然不可能直接将其暴露出来供我们修改). 我们可以向通过BuildContext操作框架层的Element树一样, 通过这个媒介来管理框架层的路由栈.

Navitator2.0的使用

Navigator2.0提供了两种管理路由的方式, 一种是改良后的Navigator方式, 一种是新的Router方式.

Navigator方式在Navigator2.0中最重大的一个改良就是引入了Page类.

Page的作用

顾名思义, Page这个类就是用来描述一个页面的. 我们通过一段代码比较来感受一下Page的作用:

在Navigator1.0中, 我们没有Page这个类, 于是注册页面的代码往往是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'App Demo',
routes: {
'/': (context) => HomePage(),
'/first': (context) => FirstPage(),
},
);
}
}


class HomePage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text(
'Home Page',
),
),
);
}

}

class FirstPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text(
'First Page',
),
),
);
}

}

而拥有了Page之后, Navigator2.0注册页面的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class MyApp extends StatefulWidget {
@override
State<StatefulWidget> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
@override
Widget build(BuildContext context) {
return Navigator(
pages: [
MaterialPage(
key: Key('home'),
name: '/',
child: HomePage(),
),
MaterialPage(
key: Key('first'),
name: '/first',
child: FirstPage(),
),
],
);
}
}

class HomePage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text(
'Home Page',
),
),
);
}
}

class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
child: Center(
child: Text(
'First Page',
),
),
);
}
}

比较两段代码, 我们可以看到, **我们传统意义上写的基于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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
abstract class Page<T> extends RouteSettings {
const Page({
this.key,
String? name,
Object? arguments,
this.restorationId,
}) : super(name: name, arguments: arguments);

/// The key associated with this page.
///
/// This key will be used for comparing pages in [canUpdate].
final LocalKey? key;

final String? restorationId;

/// Whether this page can be updated with the [other] page.
///
/// Two pages are consider updatable if they have same the [runtimeType] and
/// [key].
bool canUpdate(Page<dynamic> other) {
return other.runtimeType == runtimeType &&
other.key == key;
}

/// Creates the [Route] that corresponds to this page.
///
/// The created [Route] must have its [Route.settings] property set to this [Page].
@factory
Route<T> createRoute(BuildContext context);

@override
String toString() => '${objectRuntimeType(this, 'Page')}("$name", $key, $arguments)';
}

通过查看Page的源码, 我们可以看到刚才的猜想是正确的. 显然, 一个Page对应一个Route, 那么Page就是我们在一开始提到的, Flutter在Navigator2.0中为我们提供的操作路由栈的媒介. 那么具体是怎么操作呢?

刚才我们提到第二个有意思的点, App变成了StatefulWidget. 没错, 万能的setState来了. 既然一个Page对应一个Route, 那么我们只要往Pages里面加一个新的Page或者移除一个已有的Page, 然后再setState一下, Flutter自然就会更新路由栈了. 所以, Navigator2.0的AppState应该是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class _MyAppState extends State<MyApp> {
final List<Page> _pages = [
MaterialPage(
key: Key('home'),
name: '/',
child: HomePage(),
),
MaterialPage(
key: Key('first'),
name: '/first',
child: FirstPage(),
),
];

void push(Page newPage) {
setState(() {
_pages.add(newPage);
});
}

bool _onPop(Route<dynamic> route, dynamic result) {
setState(() {
_pages.remove(route.settings);
});
return route.didPop(result);
}

@override
Widget build(BuildContext context) {
return Navigator(
pages: _pages,
onPopPage: _onPop,
);
}
}

在这段代码中, 我们封装的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
2
3
4
5
6
7
const Router({
Key key,
this.routeInformationProvider,
this.routeInformationParser,
@required this.routerDelegate,
this.backButtonDispatcher,
})

上面是Router的构造函数, 除了Key外, 它有四个成员属性, 其中routeInformationParser和routerDelegate是使用Router方法中必须的两个组件(虽然routeInformationParser在Router里没有required, 但是在MaterialApp.router()方法和CupertinoApp.router()方法里是必须写的). 下面我们分别介绍一下这些组件.

RouteInformation

在介绍RouteInformationProvider和RouteInformationParser之前, 我们当然要了解一下RouteInformation这个概念.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/// A piece of routing information.
///
/// The route information consists of a location string of the application and
/// a state object that configures the application in that location.
///
/// This information flows two ways, from the [RouteInformationProvider] to the
/// [Router] or from the [Router] to [RouteInformationProvider].
///
/// In the former case, the [RouteInformationProvider] notifies the [Router]
/// widget when a new [RouteInformation] is available. The [Router] widget takes
/// these information and navigates accordingly.
///
/// The latter case should only happen in a web application where the [Router]
/// reports route changes back to web engine.
class RouteInformation {
const RouteInformation({this.location, this.state});

final String? location;

final Object? state;
}

上面是RouteInformation的源码和注释. 注释是这么解释RouteInformation的: 它用来描述应用当前的路由位置, 是RouteInformationProvider和Router通信的媒介. 当路由位置发生变化时, RouteInformationProvider会将新的RouteInformation告知Route, 使其重新构建; 当路由栈变化时, Router会将新的RouteInformation告知RouteInformationProvider, 使其同步给Flutter的宿主.

听起来很抽象对吧? 那么我们用大白话翻译一下:

RouteInformation是一个页面的Url, RouteInformationProvider与Flutter的宿主(在这个例子里是浏览器)同步页面Url, Route负责管理路由栈. 当用户在浏览器的Url栏里输入新的Url后, 浏览器会自动跳转的新Url对应的页面; 当浏览器跳转到新的页面(比如用户点击了浏览器的回退键), 浏览器的Url栏也会更新成新页面的Url.

RouteInformationProvider

1
2
3
abstract class RouteInformationProvider extends ValueListenable<RouteInformation?> {
void routerReportsNewRouteInformation(RouteInformation routeInformation) {}
}

上面是RouteInformationProvider的源码, 可以看到, 这个类非常简单, 就只有一个routerReportsNewRouteInformation方法. Router通过调用这个方法来通知Flutter的宿主, 路由发生了变化, 需要更新应用的路由位置. 这个类有一个默认的实现类PlatformRouteInformationProvider, Flutter就是通过它与宿主通信更新路由位置的.

RouteDelegate

RouteDelegate是Router方法中的核心, 实际上Router这个类只是一个外壳, 真正发挥作用的只是RouteDelegate, 其它的成员属性都是RouteDelegate的一个补充. 它扮演着AppState在Navigator方法中的角色——管理路由状态. 事实上, RouteDelegate的实现代码和Navigator方法中AppState的实现方式是很相似的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerDelegate: _MyRouteDelegate(),
);
}
}

class _MyRouteDelegate extends RouterDelegate<String>
with PopNavigatorRouterDelegateMixin<String>, ChangeNotifier {
final List<Page> _pages = [
MaterialPage(
key: Key('home'),
name: '/',
child: HomePage(),
),
MaterialPage(
key: Key('first'),
name: '/first',
child: FirstPage(),
),
];

void push(Page newPage) {
_pages.add(newPage);
notifyListeners();
}

bool _onPop(Route<dynamic> route, dynamic result) {
_pages.remove(route.settings);
notifyListeners();
return route.didPop(result);
}

@override
Widget build(BuildContext context) {
return Navigator(
pages: _pages,
onPopPage: _onPop,
);
}

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

@override
String get currentConfiguration =>
_pages.isNotEmpty ? _pages.last.name : null;

@override
Future<void> setInitialRoutePath(String configuration) {
return setNewRoutePath(configuration);
}

@override
Future<void> setNewRoutePath(String configuration) async {
if (configuration == '/') {
_pages.clear();
_pages.add(
MaterialPage(
key: Key('home'),
name: 'home page',
child: HomePage(),
),
);
} else if (configuration == '/first') {
_pages.clear();

_pages.add(
MaterialPage(
key: Key('home'),
name: '/',
child: HomePage(),
),
);
_pages.add(
MaterialPage(
key: Key('first'),
name: '/first',
child: FirstPage(),
),
);
}
}
}

可以看到, 使用RouteDelegate的代码和前面使用AppState的方法几乎一模一样, 只是把setState改成了notifyListeners. 如果我们查看RouteDelegate的源码, 我们会发现它混入了ChangeNotifier接口(其实RouteDelegate本身也和ChangeNotifier一样继承自Listenable, 混入Listenable只是为了不再重新实现一遍接口), 显然它是被监听的. 至于是被谁监听, 通过查找引用我们会发现, 是被Router的State——RouterState监听的, 当我们调用ChangeNotifier后, RouterState会执行setState来重新构建路由栈.

这里顺便再说一下混入的另外一个接口: PopNavigatorRouterDelegateMixin.

1
2
3
4
5
6
7
8
9
10
11
mixin PopNavigatorRouterDelegateMixin<T> on RouterDelegate<T> {
GlobalKey<NavigatorState>? get navigatorKey;

@override
Future<bool> popRoute() {
final NavigatorState? navigator = navigatorKey?.currentState;
if (navigator == null)
return SynchronousFuture<bool>(false);
return navigator.maybePop();
}
}

我们看到这个类也继承了RouterDelegate, 并实现了popRoute方法. popRoute方法是响应系统回退的回调, 这个类实现popRoute方法的逻辑是, 通过Navigator的maybePop来保证路由在没有页面的情况下不会继续pop. 如果我们想自己实现这个方法, 就可以不混入这个类.

RouteInformationParser

从上面的RouteDelegate使用实例中我们可以看到, RouteDelegate是接受泛型的, 也就是说, 我们重写的setRoutePath这些函数的参数configuration的类型是不一定的. 既然, RouteInformationProvider和Router通过RouteInformation通信, Router真正发挥作用的成员RouteDelegate使用的是一个泛型, 那么就肯定要有一个中间类把RouteInformation解析成RouteDelegate所使用的泛型, 这个类就是RouteInformationParser了.

1
2
3
4
5
6
7
8
abstract class RouteInformationParser<T> {

const RouteInformationParser();

Future<T> parseRouteInformation(RouteInformation routeInformation);

RouteInformation restoreRouteInformation(T configuration) => null;
}

上面是RouteInformationParser的源码, 这个类也很简单, 就是两个函数. 一个将RouteInformation转成T, 一个将T转成RouteInformation. 显然前者是RouteInformationProvider同步消息给Router使用的, 后者是Router同步消息给RouteInformationProvider使用的. 值得一提的是, 第二个函数不是必须要重写的, 如果不重写的话, 页面回退时Url就不会同步变化了.

BackButtonDispatcher

BackButtonDispatcher解决的就是嵌套路由响应系统回退事件的问题. 当我们有两个以上的路由嵌套, 遇到系统回退事件时, 究竟谁应该响应呢? 这个情景和事件传递的情景很像, 自然解决方法也就和事件传递的解决方法很像了.

BackButtonDispatcher有两个子类: RootBackButtonDispatcher和ChildBackButtonDispatcher. 我们可以很快想到, 前者是这个事件传递树的根节点, 负责下发事件和最终拦截事件, 后者是这个事件传递树的中间节点和叶节点, 可以下发事件也可以提前拦截事件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
abstract class BackButtonDispatcher extends _CallbackHookProvider<Future<bool>> {
late final LinkedHashSet<ChildBackButtonDispatcher> _children =
<ChildBackButtonDispatcher>{} as LinkedHashSet<ChildBackButtonDispatcher>;

@override
bool get hasCallbacks => super.hasCallbacks || (_children.isNotEmpty);

@override
Future<bool> invokeCallback(Future<bool> defaultValue) {
if (_children.isNotEmpty) {
final List<ChildBackButtonDispatcher> children = _children.toList();
int childIndex = children.length - 1;

Future<bool> notifyNextChild(bool result) {
// If the previous child handles the callback, we return the result.
if (result)
return SynchronousFuture<bool>(result);
// If the previous child did not handle the callback, we ask the next
// child to handle the it.
if (childIndex > 0) {
childIndex -= 1;
return children[childIndex]
.notifiedByParent(defaultValue)
.then<bool>(notifyNextChild);
}
// If none of the child handles the callback, the parent will then handle it.
return super.invokeCallback(defaultValue);
}

return children[childIndex]
.notifiedByParent(defaultValue)
.then<bool>(notifyNextChild);
}
return super.invokeCallback(defaultValue);
}

ChildBackButtonDispatcher createChildBackButtonDispatcher() {
return ChildBackButtonDispatcher(this);
}

void takePriority() => _children.clear();

void deferTo(ChildBackButtonDispatcher child) {
assert(hasCallbacks);
_children.remove(child); // child may or may not be in the set already
_children.add(child);
}

void forget(ChildBackButtonDispatcher child) => _children.remove(child);
}

上面是BackButtonDispatcher的源码, 其主要逻辑就是invokeCallBack方法, 这个函数也简单易懂, 就是先问子节点是否处理事件, 如果子节点不处理那么自己就处理(默认是自己不处理, 返回defaultValue=false, 如果自己想拦截就返回true). 有一个和事件分发机制不同的细节是, 这里询问子节点是倒序遍历的, 因为新的子节点是在list的后面的, 所以要从后往前询问.

源码里其它的函数基本上都是更改优先级的, forget是不让某个子节点处理事件, deferTo是把这个子节点的优先级提到最高, takePriority是不询问子节点, 自己直接处理.

至于两个子类的源码, 因为都很简单, 一个是绑定WidgetsBinding监听, 一个是管理子节点, 这里就不再介绍了.