Navigator 在Flutter中是用来做路由管理的,简单的说就是负责页面之间跳转的工作。页面之间的跳转是通过Navigator和Route共同管理完成的。
- Route 是页面的抽象,主要负责创建对应的界面,接收参数,响应 Navigator 打开和关闭;
- Navigator 则会维护一个路由栈管理 Route,Route 打开即入栈,Route 关闭即出栈,还可以直接替换栈内的某一个 Route。
根据是否需要提前注册页面标识符,Flutter 中的路由管理可以分为两种方式: - 基本路由:无需提前注册,在页面切换时需要自己构造页面实例。
- 命名路由:需要提前注册页面标识符,在页面切换时通过标识符直接打开新的路由。
下面我分别看一两种路由的使用方式
基本路由
在Flutter中导航到一个新页面,我们需要创建一个MaterialPageRoute实例,调用Navigator.push方法,MaterialPageRoute 实例作为参数传入将新页面压入栈的顶部。
MaterialPageRoute 是一种路由模板,定义了路由创建及切换过度动画的相关配置,可以针对不同的平台实现与平台页面切换动画风格一致的路由切换动画。
基本路由使用代码如下:
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
//打开页面
onPressed: ()=> Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen()));
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
// 回退页面
onPressed: ()=> Navigator.pop(context)
);
}
}
运行结果如下:
以上是最简单的路由使用方式,我们在开发中往往会涉及到页面之间传参的场景,从一个页面调到下一个页面传参可以通过属性或者构造函数的方式都可以实现,当页面返回到上一个页面在Flutter 中是如何传参的,请看下面代码:
class FirstPage extends StatefulWidget {
@override
_FirstPageState createState() => _FirstPageState();
}
class _FirstPageState extends State<FirstPage> {
var backData;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("FirstPage")),
body: Center(
child: Column(
children: [
Text("${backData??'等待返回数据...'}"),
ElevatedButton(
child: Text("Next Page"),
onPressed: () {
_navigateSecondPage(context);
},
)
],
),
),
);
}
void _navigateSecondPage(BuildContext context) async {
// ignore: avoid_print
print('执行了_navigateSecondPage');
final result =
await Navigator.push(context, MaterialPageRoute(builder: (context) {
return SecondPage();
}));
// ignore: avoid_print
print('FirstPage收到数据:$result');
setState(() {
backData = result;
});
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("SecondPage"),
),
body: Center(
child: ElevatedButton(
child: const Text("Go Back!"),
onPressed: () {
_backCurrentPage(context);
},
),
),
);
}
//退出当前页面,返回到上一级页面
void _backCurrentPage(BuildContext context) {
print('执行了_backCurrentPage');
///只有执行了这个方法,上级页面才会收到返回的数据
Navigator.pop(context, '我是来自SecondPage的数据');
}
}
运行结果如下:
命名路由
基本路由使用方式相对简单灵活,适用于应用中页面不多的场景。而在应用中页面比较多的情况下,再使用基本路由方式,那么每次跳转到一个新的页面,我们都要手动创建 MaterialPageRoute 实例,初始化页面,然后调用 push 方法打开它,还是比较麻烦的。所以,Flutter 提供了另外一种方式来简化路由管理,即命名路由。我们给页面起一个名字,然后就可以直接通过页面名字打开它了。
要想通过名字来指定页面切换,我们必须先给应用程序 MaterialApp 提供一个页面名称映射规则,即路由表 routes,这样 Flutter 才知道名字与页面 Widget 的对应关系。
路由表实际上是一个 Map,其中 key 值对应页面名字,而 value 值则是一个 WidgetBuilder 回调函数,我们需要在这个函数中创建对应的页面。而一旦在路由表中定义好了页面名字,我们就可以使用 Navigator.pushNamed 来打开页面了。
下面的代码演示了命名路由的使用方法:在 MaterialApp 完成了页面的名字 second_page 及页面的初始化方法注册绑定,后续我们就可以在代码中以 second_page 这个名字打开页面了:
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
// This widget is the root of your application.
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
routes: {
'/page1':(context) => FirstPage(),
'/page2':(context) => SecondPage(),
},
home: HomePage(),
);
}
}
class FirstPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FirstPage'),
),
body: Container(
color: Colors.white,
alignment: Alignment.center,
child: ElevatedButton(
onPressed: () {
Navigator.pushNamed(context, '/page2');
},
child: const Text(
'page1',
style: TextStyle(fontSize: 30),
),
),
),
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('SecondPage')),
body: Container(
alignment: Alignment.center,
child: ElevatedButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text(
'page2',
style: TextStyle(fontSize: 30),
),
),
),
);
}
}
运行结果如下:
我们可以看出命名路由使用起来很简单并不复杂,不过由于路由的注册和使用都采用字符串来标识,这就会带来一个隐患:如果我们打开了一个不存在的路由会怎么办?
也许你会想到,我们可以约定使用字符串常量去定义、使用路由,但我们无法避免通过接口数据下发的错误路由标识符场景。面对这种情况,无论是直接报错或是不响应错误路由,都不是一个用户体验良好的解决办法。更好的办法是,对用户进行友好的错误提示,比如跳转到一个统一的 NotFoundScreen 页面,也方便我们对这类错误进行统一收集、上报。在注册路由表时,Flutter 提供了 UnknownRoute 属性,我们可以对未知的路由标识符进行统一的页面跳转处理。下面的代码演示了如何注册错误路由处理。和基本路由的使用方法类似,我们只需要返回一个固定的页面即可。
MaterialApp(
...
//注册路由
routes:{
"second_page":(context)=>SecondPage(),
},
//错误路由处理,统一返回UnknownPage
onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),
);
//使用错误名字打开页面
Navigator.pushNamed(context,"unknown_page");
运行一下代码,可以看到,我们的应用不仅可以处理正确的页面路由标识,对错误的页面路由标识符也可以统一跳转到固定的错误处理页面了。
命名路由的接收回传参数的方式同基本路由方式,但是push传参方式更加方便,如下演示代码:
//传参方式
Navigator.pushNamed(context, '/page2', arguments: {"key":"value"});
//取参方式
ModalRoute.of(context).settings.arguments;
以上我们分别介绍了Navigator 的基本路由和命名路由的使用方式,当然push 也好,pop 也好Flutter 为我们开发者提供了很多丰富的push 或pop 的相关API,例如pushAndRemoveUntil、popUntil 等方法可以根据实际的开发需求去选择。
源码探究
上面我们从基本使用上对Navigator有了基本的了解,接下来我们从源码实现上去探究一下Navigator是如何实现路由管理的。
Navigator是什么?下面截取了Navigator的构造方法,我们从下面的源码中可以看出Navigator就是一个StatefulWidget。
class Navigator extends StatefulWidget {
/// Creates a widget that maintains a stack-based history of child widgets.
///
/// The [onGenerateRoute], [pages], [onGenerateInitialRoutes],
/// [transitionDelegate], [observers] arguments must not be null.
///
/// If the [pages] is not empty, the [onPopPage] must not be null.
const Navigator({
Key? key,
//Page 继承RouteSettings 记录了Route 的设置信息 (String? name, Object? arguments)
this.pages = const <Page<dynamic>>[],
this.onPopPage,
this.initialRoute, // 导航栈中的第一个Route 的name,也就是根Route Name
this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes,
//创建Route 的函数 传参是RouteSettings
//Route<dynamic>? Function(RouteSettings settings)
this.onGenerateRoute,
this.onUnknownRoute,
this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(),//页面切换过度动画
this.reportsRouteUpdateToEngine = false,
this.observers = const <NavigatorObserver>[],// Navigator 的监听对象
this.restorationScopeId,
})
static NavigatorState of(
BuildContext context, {
bool rootNavigator = false,
}) {
// Handles the case where the input context is a navigator element.
NavigatorState? navigator;
if (context is StatefulElement && context.state is NavigatorState) {
navigator = context.state as NavigatorState;
}
if (rootNavigator) {
///从context 中 拿到StatefulWidget 的根 state,Navigator 本身就是一个StatefulWidget
navigator = context.findRootAncestorStateOfType<NavigatorState>() ?? navigator;
} else {
///从context 中 拿到最近的StatefulWidget 的 state,Navigator 本身就是一个StatefulWidget
navigator = navigator ?? context.findAncestorStateOfType<NavigatorState>();
}
assert(() {
if (navigator == null) {
throw FlutterError(
'Navigator operation requested with a context that does not include a Navigator.\n'
'The context used to push or pop routes from the Navigator must be that of a '
'widget that is a descendant of a Navigator widget.',
);
}
return true;
}());
return navigator!;
}
static Future<T?> push<T extends Object?>(BuildContext context, Route<T> route) {
return Navigator.of(context).push(route);
}
@optionalTypeArgs
Future<T?> pushNamed<T extends Object?>(
String routeName, {
Object? arguments,
}) {
return push<T>(_routeNamed<T>(routeName, arguments: arguments)!);
}
@optionalTypeArgs
static void pop<T extends Object?>(BuildContext context, [ T? result ]) {
Navigator.of(context).pop<T>(result);
}
static void popUntil(BuildContext context, RoutePredicate predicate) {
Navigator.of(context).popUntil(predicate);
}
}
上面的代码中只截取了部分源码,详细源码请自行去sdk中查看,经过我对源码的层层探究下面对push 的流程进行一下概括。
push流程
Navigator 调用上层的push 方法MaterialPageRoute作为参数传入,首先回去调用of方法获取到NavigatorState,然后NavigatorState 去调用自己真正的push方法,push方法里调用_pushEntry方法,会把MaterialPageRoute生成一个_RouteEntry对象传入_pushEntry方法里,里面主要做了三件事_history.add(entry); _flushHistoryUpdates();_afterNavigation(entry.route); 把RouteEntry 对象加入到_hidtory list 中;更新history 中entry 的信息;性能工具做性能优化处理。
其中核心的push处理是在_flushHistoryUpdates 方法中处理,这里贴出关键代码
void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
// Clean up the list, sending updates to the routes that changed. Notably,
// we don't send the didChangePrevious/didChangeNext updates to those that
// did not change at this point, because we're not yet sure exactly what the
// routes will be at the end of the day (some might get disposed).
int index = _history.length - 1;
_RouteEntry? next;
_RouteEntry? entry = _history[index];
_RouteEntry? previous = index > 0 ? _history[index - 1] : null;
bool canRemoveOrAdd = false; // Whether there is a fully opaque route on top to silently remove or add route underneath.
Route<dynamic>? poppedRoute; // The route that should trigger didPopNext on the top active route.
bool seenTopActiveRoute = false; // Whether we've seen the route that would get didPopNext.
final List<_RouteEntry> toBeDisposed = <_RouteEntry>[];
while (index >= 0) {
switch (entry!.currentState) {
case _RouteLifecycle.add:
entry.handleAdd(
navigator: this,
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
);
continue;
case _RouteLifecycle.adding:
if (canRemoveOrAdd || next == null) {
entry.didAdd(
navigator: this,
isNewFirst: next == null,
);
continue;
}
break;
case _RouteLifecycle.push:
case _RouteLifecycle.pushReplace:
case _RouteLifecycle.replace:
entry.handlePush(
navigator: this,
previous: previous?.route,
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
isNewFirst: next == null,//
);
if (entry.currentState == _RouteLifecycle.idle) {
continue;
}
break;
case _RouteLifecycle.pushing: // Will exit this state when animation completes.
if (!seenTopActiveRoute && poppedRoute != null)
entry.handleDidPopNext(poppedRoute);
seenTopActiveRoute = true;
break;
case _RouteLifecycle.idle:
if (!seenTopActiveRoute && poppedRoute != null)
entry.handleDidPopNext(poppedRoute);
seenTopActiveRoute = true;
// This route is idle, so we are allowed to remove subsequent (earlier)
// routes that are waiting to be removed silently:
canRemoveOrAdd = true;
break;
case _RouteLifecycle.pop:
if (!seenTopActiveRoute) {
if (poppedRoute != null)
entry.handleDidPopNext(poppedRoute);
poppedRoute = entry.route;
}
entry.handlePop(
navigator: this,
previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
);
assert(entry.currentState == _RouteLifecycle.popping);
canRemoveOrAdd = true;
break;
case _RouteLifecycle.popping:
// Will exit this state when animation completes.
break;
case _RouteLifecycle.remove:
if (!seenTopActiveRoute) {
if (poppedRoute != null)
entry.route.didPopNext(poppedRoute);
poppedRoute = null;
}
entry.handleRemoval(
navigator: this,
previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
);
continue;
case _RouteLifecycle.removing:
if (!canRemoveOrAdd && next != null) {
// We aren't allowed to remove this route yet.
break;
}
entry.currentState = _RouteLifecycle.dispose;
continue;
case _RouteLifecycle.dispose:
// Delay disposal until didChangeNext/didChangePrevious have been sent.
toBeDisposed.add(_history.removeAt(index));
entry = next;
break;
case _RouteLifecycle.disposed:
case _RouteLifecycle.staging:
assert(false);
break;
}
index -= 1;
next = entry;
entry = previous;
previous = index > 0 ? _history[index - 1] : null;
}
// Informs navigator observers about route changes.
_flushObserverNotifications();
// Now that the list is clean, send the didChangeNext/didChangePrevious
// notifications.
_flushRouteAnnouncement();
// Announces route name changes.
if (widget.reportsRouteUpdateToEngine) {
final _RouteEntry? lastEntry = _history.cast<_RouteEntry?>().lastWhere(
(_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e), orElse: () => null,
);
final String? routeName = lastEntry?.route.settings.name;
if (routeName != _lastAnnouncedRouteName) {
SystemNavigator.routeUpdated(
routeName: routeName,
previousRouteName: _lastAnnouncedRouteName,
);
_lastAnnouncedRouteName = routeName;
}
}
// Lastly, removes the overlay entries of all marked entries and disposes
// them.
for (final _RouteEntry entry in toBeDisposed) {
for (final OverlayEntry overlayEntry in entry.route.overlayEntries)
overlayEntry.remove();
entry.dispose();
}
if (rearrangeOverlay) {
overlay?.rearrange(_allRouteOverlayEntries);
}
if (bucket != null) {
_serializableHistory.update(_history);
}
}
从上面的源码里看出
entry.handlePush(
navigator: this,
previous: previous?.route,
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
isNewFirst: next == null,//
);
这个handlePush 里做了具体页面如何跳转状态如何改变的工作。其内部大致实现逻辑是这样的,entry 是flush 方法遍历history的当前的RouteEntry 对象,里面的route 会持有NavigatState,然后 currentState 设置成 _RouteLifecycle.pushing 状态,之后再去调用navigator._flushHistoryUpdates(); 内部会调用entry.handleDidPopNext(poppedRoute);
以上是一个push 的大致的流程描述,下面是我整理的一张方法调用的图
pop 流程也是一个类似的过程,比push 要简单一些,拿到NavigatorState后直接flushHistory,把route 做出栈操作。