在Flutter
中,页面的跳转是通过Navigator
来实现。通过几句简单的代码就可以实现页面的跳转并传递对应的参数。那么具体实现是怎样的尼?下面就来一窥究竟。
1、根Navigator
在Flutter
中,一切皆Widget
,Navigator
也不能例外。但我们并没有主动添加Navigator
,但是又可以通过Navigator
来进行页面跳转,这是怎么回事尼?
我们在进行Flutter
开发时,必须以MaterialApp
或WidgetsApp
来作为第一个Widget
,否则就会出错。也就是在WidgetsApp
中(MaterialApp
是对WidgetsApp
的包装),Flutter
默认给我们添加了第一个Navigator
,一般跳转都是根据这个Navigator
来进行的。这也就是我们没有主动添加Navigator
,但却能够通过Navigator
来进行页面跳转的原因。如下图所示。
Widget
树中的第一个Navigator
,即根Navigator
。
对于Navigator
,我们一般都是通过Navigator.of(context)
来获取NavigatorState
对象,然后通过NavigatorState
对象中的函数来实现页面跳转、关闭、替换等。下面来看Navigator.of(context)
的实现。
static NavigatorState of(
BuildContext context, {
bool rootNavigator = false,
bool nullOk = false,
}) {
final NavigatorState navigator = rootNavigator
//获取根Navigator
? context.findRootAncestorStateOfType<NavigatorState>()
//获取距离当前Widget最近的Navigator
: context.findAncestorStateOfType<NavigatorState>();
return navigator;
}
rootNavigator
为true
表示获取根Navigator
,否则获取距离当前Widget
最近的Navigator
,默认为false
。
可以发现,这里要想获取根Navigator
,就需要传递一个context
对象,但在某些需求(如token
失效跳转登录页)下,无法拿到context
对象,但又需要跳转,那这该怎么办尼?
这时候我们可以给根Navigator
传递一个key
,然后根据这个key
拿到NavigatorState
对象。而MaterialApp
又给我们提供了向根Navigator
传递key
的机会。
class MaterialApp extends StatefulWidget {
const MaterialApp({
Key key,
//自定义key
this.navigatorKey,
...
})
}
通过给navigatorKey
赋一个GlobalKey
对象,然后通过这个对象就可以不需要context
来获得根Navigator
,从而进行页面的跳转、返回、替换等。
2、push页面源码解析
当我们正确的拿到NavigatorState
对象后,就可以通过push
函数来跳转到到新的页面。
@optionalTypeArgs
Future<T> push<T extends Object>(Route<T> route) {
//获取上一个route
final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
route._navigator = this;
//获取当前OverlayEntry对象
route.install(_currentOverlayEntry);
//将当前route添加到集合中
_history.add(route);
route.didPush();
route.didChangeNext(null);
if (oldRoute != null) {
oldRoute.didChangeNext(route);
route.didChangePrevious(oldRoute);
}
for (NavigatorObserver observer in widget.observers)
observer.didPush(route, oldRoute);
RouteNotificationMessages.maybeNotifyRouteChange(_routePushedMethod, route, oldRoute);
_afterNavigation(route);
return route.popped;
}
在这里,route
(路由)是一个非常重要的概念,它对页面进行了抽象。在Flutter
中,一个页面对应一个route
对象。_history
是NavigatorState
中的一个数组,也是通常所说的路由栈。在跳转一个新页面时,会将route
对象添加到_history
数组中,弹出页面则从该数组中移除route
对象。
在上面代码中,route.install(_currentOverlayEntry)
是页面跳转的核心实现。install
函数在Route
中是一个空实现,在其子类ModalRoute
中进行了具体实现。
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> {
......
@override
void install(OverlayEntry insertionPoint) {
super.install(insertionPoint);
_animationProxy = ProxyAnimation(super.animation);
_secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
}
......
}
在ModalRoute
里的install
函数中主要做了动画相关的处理,然后交给父类执行。在其父类TransitionRoute
主要是做动画的创建及相关处理,并交给其父类处理。那么再来看TransitionRoute
的父类OverlayRoute
。
abstract class OverlayRoute<T> extends Route<T> {
.....
/// Subclasses should override this getter to return the builders for the overlay.
Iterable<OverlayEntry> createOverlayEntries();
/// The entries this route has placed in the overlay.
@override
List<OverlayEntry> get overlayEntries => _overlayEntries;
final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];
@override
void install(OverlayEntry insertionPoint) {
//创建页面对应的两个OverlayEntry对象并加入到_overlayEntries中,以待后续处理。
_overlayEntries.addAll(createOverlayEntries());
//进行UI的更新
navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
//空实现
super.install(insertionPoint);
}
......
}
在OverlayRoute
中有一个抽象函数createOverlayEntries
,该函数需要在子类实现。代码如下。
@override
Iterable<OverlayEntry> createOverlayEntries() sync* {
yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
}
yield
的含义先不管。该函数主要是创建两个OverlayEntry
对象,一个是我们需要展示页面所对应的OverlayEntry
对象,一个是Barrier所对应的OverlayEntry
对象。它两的关系如下。
route
所对应的两个OverlayEntry
对象创建成功后,就加入到集合
_overlayEntries
中,然后调用OverlayState
的insertAll
函数。
class OverlayState extends State<Overlay> with TickerProviderStateMixin {
final List<OverlayEntry> _entries = <OverlayEntry>[];
......
void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry below, OverlayEntry above }) {
if (entries.isEmpty)
return;
for (OverlayEntry entry in entries) {
entry._overlay = this;
}
//更新UI
setState(() {
_entries.insertAll(_insertionIndex(below, above), entries);
});
}
@override
Widget build(BuildContext context) {
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(_OverlayEntry(entry));
if (entry.opaque)
onstage = false;
} else if (entry.maintainState) {
offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry)));
}
}
return _Theatre(
onstage: Stack(
fit: StackFit.expand,
children: onstageChildren.reversed.toList(growable: false),
),
offstage: offstageChildren,
);
}
......
}
在insertAll
里,就可以发现页面跳转的本质了。就是通过setState
来更新UI。
再来看OverlayState
的build
的函数,在该函数中会对需要保存在内存中的页面进行分类,主要有以下两类。
- onstageChildren:在该集合中的页面会展示在屏幕中,也就是我们能够看到的页面。
- offstageChildren:该集合中的页面存在于内存中,不会销毁,也不会展示。主要目的是为了下次加载时,方便重新加载,从而达到节省资源的目的。当
maintainState
为true时就会添加到该集合,目前面跳转的maintainState
参数默认为true。
来看一个示例。假设有三个页面page1、page2及page3。我们先给page2页面的maintainState
属性设置不同值,然后观察page2跳转到page3时page2页面的变化情况。
可以发现,当maintainState
为true时,会在内存中缓存page2页面的所有对象,以便下次操作。当然如果后面不需要再次展示page2,那么就最好将maintainState
设为false。
3、pop页面源码解析
前面分析了在flutter
中如何跳转一个新的页面,那么来看一下如何关闭一个页面。关闭页面是调用的pop
函数。
bool pop<T extends Object>([ T result ]) {
//从路由栈中获取当前页面对应的route对象
final Route<dynamic> route = _history.last;
bool debugPredictedWouldPop;
if (route.didPop(result ?? route.currentResult)) {
if (_history.length > 1) {
//从集合中移除当前页面所对应的route对象
_history.removeLast();
// If route._navigator is null, the route called finalizeRoute from
// didPop, which means the route has already been disposed and doesn't
// need to be added to _poppedRoutes for later disposal.
if (route._navigator != null)
_poppedRoutes.add(route);
_history.last.didPopNext(route);
for (NavigatorObserver observer in widget.observers)
observer.didPop(route, _history.last);
RouteNotificationMessages.maybeNotifyRouteChange(_routePoppedMethod, route, _history.last);
} else {
return false;
}
} else {}
_afterNavigation<dynamic>(route);
return true;
}
这里重点是didPop
这个函数。该函数中会调用NavigatorState
的finalizeRoute
函数来释放资源。而finalizeRoute
又会调用route
的dispose
函数。
abstract class OverlayRoute<T> extends Route<T> {
@override
bool didPop(T result) {
final bool returnValue = super.didPop(result);
assert(returnValue);
//释放资源操作
if (finishedWhenPopped)
navigator.finalizeRoute(this);
return returnValue;
}
//释放资源
@override
void dispose() {
//从_overlayEntries集合中移除当前页面及Barrier层
for (OverlayEntry entry in _overlayEntries)
entry.remove();
_overlayEntries.clear();
super.dispose();
}
}
在dispose
函数中,会将_overlayEntries
集合中清空,这里之所以是集合并遍历,是因为一个页面对应2个OverlayEntry
对象。
void remove() {
final OverlayState overlay = _overlay;
//取消OverlayEntry对OverlayState对象的引用
_overlay = null;
if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
//如果当前正在渲染UI,则会等待渲染完毕后,才更新。
SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
//更新UI
overlay._remove(this);
});
} else {
//更新UI
overlay._remove(this);
}
}
再来看OverlayState
的_remove
函数,该函数也是调用了setState
函数。然后等待UI的刷新即可。
class OverlayState extends State<Overlay> with TickerProviderStateMixin {
final List<OverlayEntry> _entries = <OverlayEntry>[];
......
void _remove(OverlayEntry entry) {
if (mounted) {
setState(() {
_entries.remove(entry);
});
}
}
......
}
再来看上一个示例,在page2页面关闭时的变化情况。
可以发现,当maintainState
为false时,从page3返回page2,会重新创建page2页面的所有对象。如果page2页面比较复杂,那么此举就比较耗资源,想必这也是maintainState
默认为true的原因吧。
4、总结
当然,Navigator
还有其他实现函数,如pushNamed
(根据routeName跳转)、pushAndRemoveUntil
(关闭并跳转到新页面)、replace
(替换当前页面)等。但这些函数的具体实现跟push
及pop
的实现基本上一致,所以只要理解了flutter
中如何进行页面跳转,那么也就基本上了解了Navigator
的的实现原理。也就知道了弹窗的打开、关闭的实现原理,因为这些操作也是通过Navigator
来实现的。
【参考资料】