所谓“命名路由”(Named Route)即有名字的路由,我们可以先给路由起一个名字,然后就可以通过路由名字直接打开新的路由了,这为路由管理带来了一种直观、简单的方式。

路由匹配规则

如果 initialRoute 设置为 /,那么加载 HomePage 页面;
如果 initialRoute 设置为 /search,在routes中存在,就加载routes中指定的路由,即SearchPage页面;
如果 initialRoute 设置为 /bag,此时routes中并不存在名称为 /bag 路由,调用onGenerateRoute;
如果 onGenerateRoute 返回路由页面,则加载此页面;
如果返回的是null,且 home参数 不为null,则加载home参数指定的页面;
如果 home 为null,则回调onUnknownRoute。

路由表

要想使用命名路由,我们必须先提供并注册一个路由表(routing table),这样应用程序才知道哪个名字与哪个路由组件相对应。其实注册路由表就是给路由起名字,路由表的定义如下:

  1. final Map<String, WidgetBuilder> routes;

它是一个Map,key为路由的名字,是个字符串;value是个builder回调函数,用于生成相应的路由widget。我们在通过路由名字打开新路由时,应用会根据路由名字在路由表中查找到对应的WidgetBuilder回调函数,然后调用该回调函数生成路由widget并返回。

路由表的注册方式很简单,在MyApp类的build方法中找到MaterialApp,添加routes属性,代码如下:

  1. import "package:flutter/material.dart";
  2. import "pages/tabs/Tabs.dart";
  3. import "pages/form/Form.dart";
  4. import "pages/search/Search.dart";
  5. void main() => runApp(MyApp());
  6. class MyApp extends StatelessWidget {
  7. @override
  8. Widget build(BuildContext context) {
  9. return MaterialApp(
  10. //默认路由
  11. initialRoute: '/test',
  12. //注册路由表
  13. routes: {
  14. "/": (context) => HomePage(),
  15. "/test": (context) => TestPage(),
  16. "/search": (context) => SearchPage({'q': 'asdf'}),
  17. //"/search": (context) => SearchPage( ModalRoute.of(context).settings.arguments ),
  18. },
  19. );
  20. }
  21. }

Navigator

pushNamed 跳转

  1. //定义
  2. static Future<T> pushNamed<T extends Object>(
  3. BuildContext context,
  4. String routeName, {
  5. Object arguments,
  6. }) {
  7. return Navigator.of(context).pushNamed<T>(routeName, arguments: arguments);
  8. }
  9. //跳转
  10. Navigator.pushNamed(
  11. context,
  12. '/search',
  13. arguments: { //传参
  14. 'id': 1,
  15. },
  16. ).then((value) {
  17. print('回传参数 $value'); //回传参数 {a: 1}
  18. });
  19. //接受参数
  20. final arg = ModalRoute.of(context).settings.arguments;
  21. print('arg $arg'); //args => {id: 1}

pushReplacementNamed 替换

  1. //定义
  2. static Future<T> pushReplacementNamed<T extends Object, TO extends Object>(
  3. BuildContext context,
  4. String routeName, {
  5. TO result,
  6. Object arguments,
  7. }) {
  8. return Navigator.of(context).pushReplacementNamed<T, TO>(routeName, arguments: arguments, result: result);
  9. }
  10. //跳转
  11. Navigator.pushReplacementNamed(
  12. context,
  13. '/search',
  14. );

pushNamedAndRemoveUntil 先跳转再删除先前路由

先将命名路由推送到Navigator,再删除先前的路由,一直到该函数的参数 predicate 返回true为止。
如果 predicate 一直返回false,则会清空先前的所有路由。

  1. //定义
  2. static Future<T> pushNamedAndRemoveUntil<T extends Object>(
  3. BuildContext context,
  4. String newRouteName,
  5. RoutePredicate predicate, {
  6. Object arguments,
  7. }) {
  8. return Navigator.of(context).pushNamedAndRemoveUntil<T>(newRouteName, predicate, arguments: arguments);
  9. }
  10. //
  11. Navigator.pushNamedAndRemoveUntil(
  12. context,
  13. '/search',
  14. (route) => false,
  15. );

popAndPushNamed 先后退再跳转

指定一个路由路径,先退出再导航到新页面。需要注意与 pushReplacement 的区别。

pushReplacement 进入 -> 销毁 popAndPushNamed 退出 -> 销毁 -> 进入

  1. //定义
  2. static Future<T> popAndPushNamed<T extends Object, TO extends Object>(
  3. BuildContext context,
  4. String routeName, {
  5. TO result,
  6. Object arguments,
  7. }) {
  8. return Navigator.of(context).popAndPushNamed<T, TO>(routeName, arguments: arguments, result: result);
  9. }
  10. //
  11. Navigator.popAndPushNamed(
  12. context,
  13. '/search',
  14. );

参数传递

https://flutter.axuer.com/docs/cookbook/navigation/navigate-with-arguments.html

通过 RouteSettings/ModalRoute 传参

在Flutter最初的版本中,命名路由是不能传递参数的,后来才支持了参数;下面展示命名路由如何传递并获取路由参数:

我们先注册路由至路由表:

  1. routes:{
  2. "/test":(context) => TestPage(),
  3. "/search":(context) => SearchPage(),
  4. } ,

在跳转路由时传递参数

  1. Navigator.pushNamed(
  2. context,
  3. '/search',
  4. arguments: { //传参
  5. 'id': 1,
  6. },
  7. )

在目标路由页通过RouteSetting对象获取路由参数:

  1. //search页面接收参数
  2. final arg = ModalRoute.of(context).settings.arguments;
  3. print('arg $arg'); //args => {id: 1}

传参适配路由表

上面我们是通过 ModalRoute 获取参数的,那么我们如何通过构造函数来获取参数呢?
答:在路由表中通过 ModalRoute 获取到参数后,再把参数传递给key对应的路由页面组件。

路由表

  1. routes: {
  2. "/": (context) => HomePage(),
  3. "/test": (context) => TestPage(),
  4. // "/search": (context) => SearchPage(),
  5. "/search": (context) => SearchPage(ModalRoute.of(context).settings.arguments),
  6. },

跳转

  1. Navigator.pushNamed(
  2. context,
  3. '/search',
  4. arguments: {
  5. 'id': 1,
  6. },
  7. )

search页面接收

  1. import "package:flutter/material.dart";
  2. class SearchPage extends StatelessWidget {
  3. SearchPage(this.args);
  4. final args;
  5. @override
  6. Widget build(BuildContext context) {
  7. print('search => ${widget.args}'); //search => {id: 1}
  8. final arg = ModalRoute.of(context).settings.arguments;
  9. print('arg $arg'); //args => {id: 1}
  10. return null;
  11. }
  12. }

onGenerateRoute 路由生成钩子

假设我们要开发一个电商APP,当用户没有登录时可以看店铺、商品等信息,但交易记录、购物车、用户个人信息等页面需要登录后才能看。为了实现上述功能,我们需要在打开每一个路由页前判断用户登录状态!如果每次打开路由前我们都需要去判断一下将会非常麻烦,那有什么更好的办法吗?答案是有!

MaterialApp有一个onGenerateRoute属性,它在打开命名路由时可能会被调用,之所以说可能,是因为当调用Navigator.pushNamed(...)打开命名路由时,如果指定的路由名在路由表中已注册,则会调用路由表中的builder函数来生成路由组件;如果路由表中没有注册,才会调用onGenerateRoute来生成路由。onGenerateRoute回调签名如下:

  1. Route<dynamic> Function(RouteSettings settings)

有了onGenerateRoute回调,要实现上面控制页面权限的功能就非常容易:我们放弃使用路由表,取而代之的是提供一个onGenerateRoute回调,然后在该回调中进行统一的权限控制,如:

  1. MaterialApp(
  2. initialRoute: '/test', //默认路由
  3. onGenerateRoute:(RouteSettings settings){
  4. return MaterialPageRoute(builder: (context){
  5. String routeName = settings.name;
  6. // 如果访问的路由页需要登录,但当前未登录,则直接返回登录页路由,
  7. // 引导用户登录;其它情况则正常打开路由。
  8. }
  9. );
  10. }
  11. );

注意,onGenerateRoute只会对命名路由生效。

简单封装

main.dart
  1. import "package:flutter/material.dart";
  2. import "package:app1/routes/RoutesOne.dart";
  3. void main() => runApp(MyApp());
  4. class MyApp extends StatelessWidget {
  5. @override
  6. Widget build(BuildContext context) {
  7. return MaterialApp(
  8. initialRoute: '/', //默认路由
  9. onGenerateRoute: RoutesOne,
  10. );
  11. }
  12. }

routes/RoutesOne.dart
  1. import "package:flutter/material.dart";
  2. import 'package:app1/pages/home/index.dart';
  3. import 'package:app1/pages/test/index.dart';
  4. import 'package:app1/pages/search/index.dart';
  5. import 'package:app1/pages/bag/index.dart';
  6. final Map<String, Function> myRoutes = {
  7. "/": (context) => HomePage(),
  8. "/test": (context) => TestPage(),
  9. "/search": (context, {arguments}) => SearchPage(arguments),
  10. "/bag": (context, {arguments}) => BagPage(id: arguments.id),
  11. };
  12. Function RoutesOne = (RouteSettings settings) {
  13. final String name = settings.name;
  14. final Function myRoutesBuilder = myRoutes[name];
  15. return MaterialPageRoute(
  16. builder: (context) {
  17. if (myRoutesBuilder == null) {
  18. return Text('未发现路由页面');
  19. } else {
  20. if (settings.arguments == null) {
  21. return myRoutesBuilder(context);
  22. } else {
  23. return myRoutesBuilder(context, arguments: settings.arguments);
  24. }
  25. }
  26. },
  27. );
  28. };

A页面
  1. Navigator.pushNamed(
  2. context,
  3. '/search',
  4. arguments: {
  5. 'id': 1,
  6. },
  7. )

search页面
  1. import "package:flutter/material.dart";
  2. class SearchPage extends StatelessWidget {
  3. SearchPage(this.args);
  4. final args;
  5. @override
  6. Widget build(BuildContext context) {
  7. print('search => ${widget.args}'); //search => {id: 1}
  8. return null;
  9. }
  10. }

总结

本章先介绍了Flutter中路由管理、传参的方式,然后又着重介绍了命名路由相关内容。在此需要说明一点,由于命名路由只是一种可选的路由管理方式,在实际开发中,可能心中会犹豫到底使用哪种路由管理方式。在此,建议最好统一使用命名路由的管理方式,这将会带来如下好处:

  1. 语义化更明确。
  2. 代码更好维护;如果使用匿名路由,则必须在调用Navigator.push的地方创建新路由页,这样不仅需要import新路由页的dart文件,而且这样的代码将会非常分散。
  3. 可以通过onGenerateRoute做一些全局的路由跳转前置处理逻辑。

综上所述,比较建议使用命名路由,可以根据自己偏好或实际情况来决定。

另外,比如路由MaterialApp中还有navigatorObserversonUnknownRoute两个回调属性,前者可以监听所有路由跳转动作,后者在打开一个不存在的命名路由时会被调用,由于这些功能并不常用,用到时可以自行查看API文档