开始
无论app还是webapp,路由都是必不可少的,相对于webapp,app的路由一般都更加强大和可控,这方面web实在太欠缺,而Flutter很明显完全克服了web的缺点,拥有一个更为完善的路由模块,这也是Flutter整个框架的特点,吸收web开发优点,但也克服web那些显而易见的缺点,提供一个更为轻松高效的开发环境,好吧,接下来一起深入了解这个模块吧。
从哪里返回
可以理解Flutter仅仅提供一个View层,其实相当多的功能都要依赖原生,例如电池信息,位置信息,网络信息等等,更为简单的说Flutter就是Super WebView。
同样当我们按下返回键时,就需要原生层告诉Flutter,弹出一个路由,让它返回上一级页面。
来到WidgetsBinding.initInstances方法:
void initInstances() {
...
SystemChannels.navigation.setMethodCallHandler(_handleNavigationInvocation);
...
}
接着_handleNavigationInvocation方法:
Future<dynamic> _handleNavigationInvocation(MethodCall methodCall) {
switch (methodCall.method) {
case 'popRoute':
return handlePopRoute();
case 'pushRoute':
return handlePushRoute(methodCall.arguments);
}
return new Future<Null>.value();
}
根据原生层的调用选择弹出一个路由或者压入一个路由,现在主要追踪弹出路由的处理。
接着handlePopRoute方法:
Future<Null> handlePopRoute() async {
for (WidgetsBindingObserver observer in new List<WidgetsBindingObserver>.from(_observers)) {
if (await observer.didPopRoute())
return;
}
SystemNavigator.pop();
}
这里也是一个观察者模式的实现了,通知所有监听者弹出路由,这里主要一个处理就是如果didPopRoute返回false(也就是没有路由愿意处理)就交给系统默认处理,一般要么退出这个Activity,要么退出应用。
那么在哪里注册这个监听器并与Navigator组件关联起来的尼?
答案就在_WidgetsAppState这类里面:
void initState() {
...
WidgetsBinding.instance.addObserver(this);
}
再看一下这个类的didPopRoute和didPushRoute方法:
// On Android: the user has pressed the back button.
@override
Future<bool> didPopRoute() async {
final NavigatorState navigator = _navigator.currentState;
return await navigator.maybePop();
}
@override
Future<bool> didPushRoute(String route) async {
final NavigatorState navigator = _navigator.currentState;
navigator.pushNamed(route);
return true;
}
在这里就把Navigator关联起来了。
Navigator和Route
Navigator的职责是负责管理Route的,管理方式就是利用一个栈不停压入弹出,当然也可以直接替换其中某一个Route。而Route作为一个管理单元,主要负责创建对应的界面,响应Navigator压入路由和弹出路由。
Flutter定义路由的方式跟前端MVC框架是很相似的,你会看到有这种类似的:/home,/posts,/posts/:id等等,搞前端的同学应该想到熟悉。
弹出路由
接着继续追踪路由弹出处理流程,看一下NavigatorState.maybePop方法:
Future<bool> maybePop([dynamic result]) async {
final Route<dynamic> route = _history.last;
assert(route._navigator == this);
final RoutePopDisposition disposition = await route.willPop();
if (disposition != RoutePopDisposition.bubble && mounted) {
if (disposition == RoutePopDisposition.pop)
pop(result);
return true;
}
return false;
}
这个时候,Navigator会询问Route是否要自己处理还是交给系统处理,当Route.willPop返回值为RoutePopDisposition.bubble时即交给系统处理,这里也简单介绍RoutePopDisposition三个枚举值:
pop 弹出路由,正常情况返回上一级
doNotPop 不弹出,沉默处理,很多时候出现在一些表单填写的情况,必须完成页面内容,或者提示用户点击第二次才能退出
bubble 交给系统处理,一般直接退出应用
再看一下Route.willPop默认实现:
Future<RoutePopDisposition> willPop() async {
return isFirst ? RoutePopDisposition.bubble : RoutePopDisposition.pop;
}
先会判断自身是否是最后一个路由,如果是交给系统处理退出应用,如果不是弹出一个路由,很正常的行为实现。
所以当返回的是pop时,调用Navigator.pop方法:
bool pop([dynamic result]) {
final Route<dynamic> route = _history.last;
bool debugPredictedWouldPop;
if (route.didPop(result ?? route.currentResult)) {
if (_history.length > 1) {
setState(() {
_history.removeLast();
if (route._navigator != null)
_poppedRoutes.add(route);
_history.last.didPopNext(route);
for (NavigatorObserver observer in widget.observers)
observer.didPop(route, _history.last);
});
} else {
return false;
}
} else {
assert(!debugPredictedWouldPop);
}
_cancelActivePointers();
return true;
}
在弹出路由前,会调用Route.didPop方法,也可以看到就算之前Route.willPop返回值为pop,仍然可以在Route.didPop返回false改变这个行为,从而不弹出路由。
但是如果Route.didPop方法返回的是true,就会把当前路由弹出,并调起现在当前的路由didPopNext方法通知它已经回到前台,做好一些状态恢复工作,例如:拉取最新列表信息。
而最后的_cancelActivePointers方法,立刻分发一个PointerCancel事件,这个时候手势识别器的状态会被重置,例如:双击手势,刚点了第一下就按了返回键,就会重置状态。
接着Route.didPop方法,当我们退出一个页面的时候一般都会执行一个过渡动画,但是过渡动画的持续时间多少,Navigator无法知道,所以Route要自己负责调起NavigatorState.finalizeRoute方法,通知Navigator释放路由,然后Navigator会回调Route.dispose方法释放Route自身资源,路由的生命周期结束。
压入路由
直接来到NavigatorState.pushNamed方法:
Future<dynamic> pushNamed(String name) {
return push(_routeNamed(name));
}
再跳到_routeNamed方法:
Route<dynamic> _routeNamed(String name, { bool allowNull: false }) {
final RouteSettings settings = new RouteSettings(
name: name,
isInitialRoute: _history.isEmpty,
);
Route<dynamic> route = widget.onGenerateRoute(settings);
if (route == null && !allowNull) {
route = widget.onUnknownRoute(settings);
}
return route;
}
也很简单根据名称查找路由,可以看看MaterialApp的实现:
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
final String name = settings.name;
WidgetBuilder builder;
if (name == Navigator.defaultRouteName && widget.home != null)
builder = (BuildContext context) => widget.home;
else
builder = widget.routes[name];
if (builder != null) {
return new MaterialPageRoute<dynamic>(
builder: builder,
settings: settings,
);
}
if (widget.onGenerateRoute != null)
return widget.onGenerateRoute(settings);
return null;
}
我们创建MaterialApp时都会传入一个route的map,然后就是根据map来创建route,就这么简单。
如果找到路由,就像我们访问网页404,应该给一个友好的页面告诉用户不存在,就可以在onUnknownRoute回调中返回一个页面。
接着NavigatorState.push方法:
Future<dynamic> push(Route<dynamic> route) {
setState(() {
final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
route._navigator = this;
route.install(_currentOverlayEntry);
_history.add(route);
route.didPush();
route.didChangeNext(null);
if (oldRoute != null)
oldRoute.didChangeNext(route);
for (NavigatorObserver observer in widget.observers)
observer.didPush(route, oldRoute);
});
_cancelActivePointers();
return route.popped;
}
重点看Route.install方法,首先了解一下Route的继承关系:
可以看到Route的实现都继承自OverlayRoute,而OverlayRoute.install的实现:
void install(OverlayEntry insertionPoint) {
assert(_overlayEntries.isEmpty);
_overlayEntries.addAll(createOverlayEntries());
navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
super.install(insertionPoint);
}
navigator会把Route.createOverlayEntries创建的OverlayEntries添加到自己的Overlay组件上;而createOverlayEntries方法干了啥尼,再来到ModalRoute.createOverlayEntries:
Iterable<OverlayEntry> createOverlayEntries() sync* {
yield new OverlayEntry(builder: _buildModalBarrier);
yield new OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
}
可以看到构建两个OverlayEntry,我们经常看到对话框后面还有一层遮罩,就是由这里产生的。
再看一下OverlayEntry有两个重要的属性opaque和maintainState,当我们把OverlayEntry添加到Navigator的Overlay组件时,Overlay组件构建过程处理是这样的:
Widget build(BuildContext context) {
// These lists are filled backwards. For the offstage children that
// does not matter since they aren't rendered, but for the onstage
// children we reverse the list below before adding it to the tree.
final List<Widget> onstageChildren = <Widget>[];
final List<Widget> offstageChildren = <Widget>[];
bool onstage = true;
for (int i = _entries.length - 1; i >= 0; i -= 1) {
final OverlayEntry entry = _entries[i];
if (onstage) {
onstageChildren.add(new _OverlayEntry(entry));
if (entry.opaque)
onstage = false;
} else if (entry.maintainState) {
offstageChildren.add(new TickerMode(enabled: false, child: new _OverlayEntry(entry)));
}
}
return new _Theatre(
onstage: new Stack(
fit: StackFit.expand,
children: onstageChildren.reversed.toList(growable: false),
),
offstage: offstageChildren,
);
}
当有一个OverlayEntry的opaque为true时(就是不透明看不到下面的页面),默认情况下在它之下OverlayEntry不会实例化(也没必要),但是如果设置maintainState为true时,OverlayEntry会build出组件树,但是这些组件不会被布局和绘制,主要用于维持组件状态。
当Route.dipose方法调起后,Route的OverlayEntry才会从Navigator的Overlay组件移除。
再回头当Route.install方法调起后,一般过渡动画会在这里构建,接会调用Route.didPush方法,过渡动画会在这里播放,最后再调用前一个路由的didChangeNext方法通知它被退到后台,可以在这个方法里保存自己状态信息,等下次回到前台时恢复。
值得注意的地方
在我们初始化的时候,我们默认初始化路由是:‘/’,但是我们如果初始化路由是这样的:‘/posts/123’,框架是怎样处理的尼?
我们来到NavigatorState.initState方法
void initState() {
super.initState();
for (NavigatorObserver observer in widget.observers) {
assert(observer.navigator == null);
observer._navigator = this;
}
String initialRouteName = widget.initialRoute ?? Navigator.defaultRouteName;
if (initialRouteName.startsWith('/') && initialRouteName.length > 1) {
initialRouteName = initialRouteName.substring(1); // strip leading '/'
assert(Navigator.defaultRouteName == '/');
final List<String> plannedInitialRouteNames = <String>[
Navigator.defaultRouteName,
];
final List<Route<dynamic>> plannedInitialRoutes = <Route<dynamic>>[
_routeNamed(Navigator.defaultRouteName, allowNull: true),
];
final List<String> routeParts = initialRouteName.split('/');
if (initialRouteName.isNotEmpty) {
String routeName = '';
for (String part in routeParts) {
routeName += '/$part';
plannedInitialRouteNames.add(routeName);
plannedInitialRoutes.add(_routeNamed(routeName, allowNull: true));
}
}
if (plannedInitialRoutes.contains(null)) {
push(_routeNamed(Navigator.defaultRouteName));
} else {
for (Route<dynamic> route in plannedInitialRoutes)
push(route); //连续压入
}
} else {
Route<dynamic> route;
if (initialRouteName != Navigator.defaultRouteName)
route = _routeNamed(initialRouteName, allowNull: true);
if (route == null)
route = _routeNamed(Navigator.defaultRouteName);
push(route);
}
for (Route<dynamic> route in _history)
_initialOverlayEntries.addAll(route.overlayEntries);
}
整个处理也很简单,直接把/posts和/posts/123压入路由栈中,而不是仅仅只是把/posts/123压进去,这跟web的url跳转就有点出入了,值得注意。