这里写自定义目录标题
Flutter命名路由及传参的深度实践与解读
在写Flutter应用时,实现页面间交叉跳转时,通常多是使用命名路由,这样更方便。但是经常会遇到某些情况不能达到理想的效果,网罗众文基本都千篇一律,大多数是源自Flutter中文官网或几个前期博文的加工再发,没有针对Flutter的命名路由特性进行深度的分析。本人在近期项目需求中遇到了路由问题,顺便对Flutter命名路由做了深度实践,记录发文,希望对后来者有所帮助!
.
本次深入实践的起因
在我们的一个项目中有个页面暂称为“Page4”,会有不同的页面在不同的路由位置跳转到这个页面,但是需要在这个页面点确定按钮时,跳转到当前路由的前N个页面,例如是“Page2”(Page4是经过Page2从首页一路加载过来的),并刷“Page2”。然后要求在“Page2”执行pop方法应该可以返回到“Page1”页面。
期望流程及路由记录如下:
- 从首页进入到Page4:Home → Page1 → Page2→ Page3 → Page4
- 在Page4点确定按钮:Home → Page1 → Page2(新创建的)
- 在Page2点返回按钮:Home → Page1
在Flutter的路由方法里,我们发现Navigator.pushNamedAndRemoveUntil方法携带ModalRoute.withName参数,刚好是我们需要的。这个方法携带该参数后,是从最近向前删除路由到指定的路由为止(不是清空),然后再创建指定的页面(新创就等于刷新了)。然后我们在使用时,发现跳转没问题,跳转后再点返回出现了黑屏,网罗了一大堆原因均不吻合。后又遇到命名路由传参问题,经仔细实践,更深入了解路由的原理,最终一并得以解决。
虽然有其他实现方法可以达到目的,我们不在这里讨论,在这只探讨Flutter命名路由的深入实践过程及得出的结果!
.
跳转后点返回黑屏或退出应用的BUG再现
BUG示例代码:
main.dart
import 'package:flutter/material.dart';
import 'home.dart';
import 'routes.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '演示命名路由使用',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Home(),
routes: routes,
onGenerateRoute: onGenerateRoute,
);
}
}
.
home.dart
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
Home({Key key}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('命名路由示例'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Flutter 欢迎您!'),
RaisedButton(
child: Text("pushNamed跳page01页"),
onPressed: () {
Navigator.pushNamed(context, '/page01', arguments: '这是来自Home页的数据');
},
),
],
),
),
);
}
}
.
page01.dart
import 'package:flutter/material.dart';
class Page01 extends StatefulWidget {
Page01({Key key, this.param}) : super(key: key);
final String param;
@override
_Page01State createState() => _Page01State(msg:this.param);
}
class _Page01State extends State<Page01> {
_Page01State({this.msg});
String msg;
@override
void initState() {
print('类初始化收到:' + (msg ?? '空')); // 控制台输出msg参数,监测传值变化
super.initState();
}
@override
Widget build(BuildContext context) {
String args = ModalRoute.of(context).settings.arguments;
print('settings收到:' + (args ?? '空')); // 控制台输出args参数,监测传值变化
return Scaffold(
appBar: AppBar(title: Text('页面 page01')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('类初始化收到:' + (msg ?? '空')),
Text('settings收到:' + (args ?? '空')),
RaisedButton(
child: Text("pushNamed跳page02页"),
onPressed: () {
Navigator.pushNamed(context, '/page02', arguments: '这是来自page01页的数据');
},
),
],
),
),
);
}
}
.
page02.dart、page03.dart
与page01.dart页代码基本一样,仅标题和传递参数略有差别,这里不再复制
.
page04.dart
import 'package:flutter/material.dart';
class Page04 extends StatefulWidget {
Page04({Key key, this.param}) : super(key: key);
final String param;
@override
_Page04State createState() => _Page04State(msg:this.param);
}
class _Page04State extends State<Page04> {
_Page04State({this.msg});
String msg;
@override
void initState() {
print('类初始化收到:' + (msg ?? '空')); // 控制台输出msg参数,监测传值变化
super.initState();
}
@override
Widget build(BuildContext context) {
String args = ModalRoute.of(context).settings.arguments;
print('settings收到:'+(args ?? '空')); // 控制台输出args参数,监测传值变化
return Scaffold(
appBar: AppBar(title: Text('页面 page04')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('类初始化收到:' + (msg ?? '空')),
Text('settings收到:' + (args ?? '空')),
RaisedButton( // A跳转按钮
child: Text("保留路由新建page01页"),
onPressed: () {
Navigator.pushNamed(context, '/page01', arguments:'pushNamed');
},
),
RaisedButton( // B跳转按钮
child: Text("删部分路由后新建page01页"),
onPressed: () {
Navigator.pushNamedAndRemoveUntil(context, '/page01', ModalRoute.withName('/home'), arguments:'pushNamedAndRemoveUntil');
},
),
],
),
),
);
}
}
.
routes.dart
import 'package:flutter/material.dart';
import 'home.dart';
import 'page01.dart';
import 'page02.dart';
import 'page03.dart';
import 'page04.dart';
final routes = {
'/home': (context) => Home(),
'/page01': (context, {arguments}) => Page01(param:arguments),
'/page02': (context, {arguments}) => Page02(param:arguments),
'/page03': (context, {arguments}) => Page03(param:arguments),
'/page04': (context, {arguments}) => Page04(param:arguments),
};
Route<dynamic> onGenerateRoute(RouteSettings settings) {
String routeName = settings.name;
print('当前访问路由名:$routeName');
final Function pageContentBuilder = routes[routeName];
if (pageContentBuilder != null) {
if (settings.arguments != null) {
final Route route = MaterialPageRoute(
builder: (context) => pageContentBuilder(context, arguments: settings.arguments),
);
return route;
} else {
final Route route = MaterialPageRoute(
builder: (context) => pageContentBuilder(context),
);
return route;
}
}
return null;
}
.
执行pop黑屏或退出应用的原由
从首页home页面,首次一路点击按钮过来,在page04当前页时,路由栈内顺序应该是:
[’/home’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’]
但实际上里面没有存在 ‘/home’,因为创建 home 页面时没有使用命名路由,所以路由栈内的名字是:
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’],’/'是路由管理器默认给的初始化名字(具体名字不一定是这个)
在 page04.dart 的 A跳转按钮 事件中,我们使用的是 pushNamed 方法,实现的结果与我们预期的是一致的。也就是之前路由中有一个 page02 页面,在当前 page04 页面之后再新建一个 page02(因为要刷新,所以直接使用了新建),这样返回的话需要点5次返回才到首页,而期望是返回2次即到首页。所以尝试使用 B跳转按钮 的事件。
点 A跳转按钮 后,路由栈内是:
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’, ‘/page02’]
在 page04.dart 的 B跳转按钮 事件中,我们使用的是 pushNamedAndRemoveUntil 方法,期望清掉路由栈中的 ‘page02’, ‘page03’, ‘page04’,然后创建一个新的 page02。
点 B跳转按钮 后期望路由栈内是:
[’/home’, ‘/page01’, ‘/page02’]
但当前代码结果实际不是,经过实践以为找不到 ‘/home’ 和 ‘/page01’ ,则把路由栈全清空了,实际是:
[’/page02’]
也就是只有 page02 页面了,没有上一页了,所以这是调用pop方法返回上一页,找不到 page01了,这是黑屏或退出现象的主因。
.
实现类初始化接收不到参数的原由
经过测试发现,如果使用上述命名路由的方式,虽然在路由Map中,page01-04都配置了参数传递,但是相应页面中的实现类初始化接参都是空的。经过反复实践,发现原因出在以下几点:
- 路由Map中的“(context, {arguments}) => Page01(param:arguments)”这种写法要配合路由拦截器才有效,否则传递不过去参数。
- main.dart 中 MaterialApp 配置了 routes 属性并指向了 路由Map(routes.dart中的routes),这样只要在routes中能找到的页面名字,就不会触发路由拦截器,因此参数传递也就不可能有效。
.
深度实践整理后的解读
.
深度实践后的Flutter命名路由示例代码
下面是示例应用截图:
.
代码中关键位置标有详细注释,这均是经过实践得出的结论,若有错误请以官方文档为准。
main.dart
import 'package:flutter/material.dart';
import 'routes.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '演示命名路由使用',
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
//home: Home(),
// 应用初始化时第一个要加载的路由,通常称为根路由,不使用这个配置会造成查不到根路由页面的名称
//initialRoute: '/',
// 命名路由Map,创建页面是这里有的就不会触发onGenerateRoute事件,不使用则直接触发onGenerateRoute事件
//routes: routes,
// 路由拦截器,产生新路由页面时,如果在路由列表routes中查不到,则会触发这个事件
// 所以没有给routes属性赋值的话,则执行任何一个命名路由相关的创建页面方法都会触发本事件
onGenerateRoute: onGenerateRoute,
);
}
}
.
home.dart
import 'package:flutter/material.dart';
class Home extends StatefulWidget {
Home({Key key}) : super(key: key);
@override
_HomeState createState() => _HomeState();
}
class _HomeState extends State<Home> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('命名路由示例-首页'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text('Flutter 欢迎您!'),
RaisedButton(
child: Text("pushNamed创建page01页"),
onPressed: () {
Navigator.pushNamed(context, '/page01', arguments: '这是来自Home页的数据');
},
),
],
),
),
);
}
}
.
page01.dart
import 'package:flutter/material.dart';
class Page01 extends StatefulWidget {
// 初始化时接收创建类时传递的参数,保存在param
Page01({Key key, this.param}) : super(key: key);
// 这个一定要是终极类型,因为本类直接继承自StatefulWidget(被标记为@immutable)
// 为适用命名路由表,这里只设一个参数,需要多个可以传对象或Map
// 名字不是必须为arguments,但是要与路由配置中参数名称一致,见routes.dart的'/page01'一行
final String param;
// 为_Page01State类中可以在build外方便获取param,这里传递this.param给_Page01State的msg
@override
_Page01State createState() => _Page01State(msg:this.param);
}
class _Page01State extends State<Page01> {
// 初始化获取Page01传递过来的param给本类的msg
_Page01State({this.msg});
// 这里可以不是终极类型
String msg = '空';
@override
void initState() {
// 控制台输出msg参数,监测传值变化
print('Page01类初始化收到:$msg');
super.initState();
}
@override
Widget build(BuildContext context) {
// 通过ModalRoute.of(context).settings.arguments获取的参数,仅在build内可以
String args = ModalRoute.of(context).settings.arguments;
if (args == null) args = '空';
// 控制台输出args参数,监测传值变化
print('page01页settings收到:$args');
return Scaffold(
appBar: AppBar(
// 标记是第几个页面,便于测试识别
title: Text('页面 page01'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 页面输出类初始化收到的参数情况,识别更直观
Text('类初始化收到:$msg'),
// 页面输出类在build内使用ModalRoute的settings收到的参数情况,识别更直观
Text('settings收到:$args'),
RaisedButton(
child: Text("pushNamed创建page02页"),
onPressed: () {
// 通过pushNamed方法跳转到page02,并传参“这是来自page01页的数据”
Navigator.pushNamed(context, '/page02', arguments: '这是来自page01页的数据');
},
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: () {
// 通过popUntil方法直接返回首页home,不传参,便于测试
// 此方法指定的页面名称必需在当前路由表中存在,否则会黑屏(例如之前有过清空路由操作)
Navigator.popUntil(context, ModalRoute.withName('/'));
},
child: Text('首页'),
),
);
}
}
page02.dart、page03.dart
与page01.dart代码基本相同,只是传参与描述与页面名称相关的更改一下即可,这里就不复制
.
page04.dart
import 'package:flutter/material.dart';
class Page04 extends StatefulWidget {
// 初始化时接收创建类时传递的参数,保存在param
Page04({Key key, this.param}) : super(key: key);
// 这个一定要是终极类型,因为本类直接继承自StatefulWidget(被标记为@immutable)
// 为适用命名路由表,这里只设一个参数,需要多个可以传对象或Map
// 名字不是必须为arguments,但是要与路由配置中参数名称一致,见routes.dart的'/page01'一行
final String param;
// 为_Page01State类中可以在build外方便获取param,这里传递this.param给_Page01State的msg
@override
_Page04State createState() => _Page04State(msg:this.param);
}
class _Page04State extends State<Page04> {
// 初始化获取Page01传递过来的param给本类的msg
_Page04State({this.msg});
// 这里可以不是终极类型
String msg = '空';
@override
void initState() {
// 控制台输出msg参数,监测传值变化
print('Page04类初始化收到:$msg');
super.initState();
}
@override
Widget build(BuildContext context) {
// 通过ModalRoute.of(context).settings.arguments获取的参数,仅在build内可以
String args = ModalRoute.of(context).settings.arguments;
if (args == null) args = '空';
// 控制台输出args参数,监测传值变化
print('page02页settings收到:$args');
return Scaffold(
appBar: AppBar(
// 标记是第几个页面,便于测试识别
title: Text('页面 page04'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
// 页面输出类初始化收到的参数情况,识别更直观
Text('类初始化收到:$msg'),
// 页面输出类在build内使用ModalRoute的settings收到的参数情况,识别更直观
Text('settings收到:$args'),
RaisedButton(
child: Text("A:保留路由新建page02页"),
onPressed: () {
// 通过pushNamed方法创建page02,并传参方法名
// 此方法保存原路由栈,新创建一个page02页面,并压入路由栈
// 执行后,路由栈内应该是['/home', '/page01', '/page02', '/page03', '/page04', '/page02']
Navigator.pushNamed(context, '/page02', arguments:'pushNamed');
},
),
RaisedButton(
child: Text("B:删部分路由后新建page02页"),
onPressed: () {
// 通过pushNamedAndRemoveUntil方法带ModalRoute.withName配置创建page02,并传参方法名
// 此方法将从路由栈顶部(最近)开始逐一弹出(删除),直到遇到'/home'停止删除('/home'不会被删除),再创建page02
// 执行后,路由栈里就只剩下了['/home', '/page01', '/page02'],page02的上一页就是page01,而不是当前页page04
Navigator.pushNamedAndRemoveUntil(context, '/page02', ModalRoute.withName('/page01'), arguments:'pushNamedAndRemoveUntil');
},
),
RaisedButton(
child: Text("C:清空路由栈后新建page02页"),
onPressed: () {
// 通过pushNamedAndRemoveUntil带(Route<dynamic> route)=>false配置创建page02,并传参方法名
// 此方法将清空路由栈(弹出/删除所有路由),然后再创建page02
// 执行后,路由栈里就只有['/page02'],page02变成了根页面,没有上一页了
Navigator.pushNamedAndRemoveUntil(context, '/page02', (Route<dynamic> route)=>false, arguments:'pushNamedAndRemoveUntil');
},
),
RaisedButton(
child: Text("D:替换当前路由无转场效果新建page02页"),
onPressed: () {
// 通过pushReplacementNamed方法跳转到page02,并传参方法名
// 此方法将在原路由栈替换掉当前的路由(page04),然后再创建page02
// 执行后,路由栈里是['/home', '/page01', '/page02', '/page03', '/page02'],page02的上一页就是page03,而不是当前页page04
Navigator.pushReplacementNamed(context, '/page02', arguments:'pushReplacementNamed');
},
),
RaisedButton(
child: Text("E:替换当前路由有转场效果新建page02页"),
onPressed: () {
// 通过popAndPushNamed方法跳转到page02,并传参方法名
// 此方法与pushReplacementNamed方法最终在路由栈里的结果相同
// 只是执行过程中,此方法有转场效果,而pushReplacementNamed方法没有
Navigator.popAndPushNamed(context, '/page02', arguments:'pushReplacementNamed');
},
),
RaisedButton(
child: Text("F:删除部分路由回到page02页"),
onPressed: () {
// 通过popUntil方法返回到page02,此方法无法传参,也不触发路由拦截器
// 此方法与pushNamedAndRemoveUntil方法最终在路由栈里的结果相同
// 只是page02不是新建的,不刷新页面保持原有状态,等同于连续的pop;而pushNamedAndRemoveUntil是新建页面,类似于刷新了页面
// 此方法指定的页面名称必需在当前路由表中存在,否则会黑屏(例如之前有过清空路由操作)
Navigator.popUntil(context, ModalRoute.withName('/page02'));
},
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: () {
// 通过popUntil方法直接返回首页home,不传参,便于测试
// 此方法指定的页面名称必需在当前路由表中存在,否则会黑屏(例如之前有过清空路由操作)
Navigator.popUntil(context, ModalRoute.withName('/'));
},
child: Text('首页'),
),
);
}
}
routes.dart
import 'package:flutter/material.dart';
import 'home.dart';
import 'page01.dart';
import 'page02.dart';
import 'page03.dart';
import 'page04.dart';
final routes = {
// 如果标注'/',而main.dart中又没有指定home页,但指定routes为本Map,则应用初始化时自动加载'/'指定的页面
// 若是main.dart中指定了home页,指定routes为本Map,这里就不要有'/',否则会报警告
'/': (context) => Home(),
// 这里的“arguments”必须是这个名字,否则取不到通过“pushNamed”等方法传递的参数
// 这里的“param”是与对应的类中的接收参数命名对应的,不是一定要使用“arguments”这个名字
// 这里的参数传递仅对使用onGenerateRoute路由拦截器,并在拦截器内写入参数传递的代码并触发了拦截器时才有效
// 这里的参数传递是传递给类初始化的参数,与ModalRoute.of(context).settings.arguments无关
//
// 若不需要给类初始化传参,可以写成 '/page01': (context) => Page01()这样
// 同样可以用ModalRoute.of(context).settings.arguments接收“pushNamed”等方法传递的参数
'/page01': (context, {arguments}) => Page01(param:arguments),
'/page02': (context, {arguments}) => Page02(param:arguments),
'/page03': (context, {arguments}) => Page03(param:arguments),
'/page04': (context, {arguments}) => Page04(param:arguments),
};
// 路由拦截器,只有在MaterialApp的routes属性没有赋值的情况下,才会每次创建页面都触发
// 如果指定了routes属性,那只有routes中查不到的时候才会触发
Route<dynamic> onGenerateRoute(RouteSettings settings) {
String routeName = settings.name;
// 控制台输出当前拦截的路由名称,方便清晰的理解路由创建流畅
print('当前访问路由名:$routeName');
// 为了避免报错,将在命名路由列表查不到的都指向首页(根页)
if (!routes.containsKey(routeName)) routeName = '/';
// 获取页名对应的类
final Function pageContentBuilder = routes[routeName];
if (settings.arguments != null) {
// 如果带有参数,则调用该类的时候,需要将参数传递过去
return MaterialPageRoute(
builder: (context) => pageContentBuilder(context, arguments: settings.arguments),
// 这一行是命名路由顺畅应用的关键,否则新创建的页面压入路由栈的将不是自己定义的名字
// 主要是使用 settings.name 的作用,为新创建的页面命名
settings: settings,
);
} else {
// 如果没有带参数,则调用该类的时候,不需要传递参数
return MaterialPageRoute(
builder: (context) => pageContentBuilder(context),
settings: settings,
);
}
}
.
Flutter命名路由示例用到的路由方法
- pushNamed
保留当前路由栈,在命名路由列表中,根据名称查找对应的页面实现类并创建该页,然后压入路由栈。
可以携带参数(非必传),若要传参必需以arguments命名,要传多个参数可封装成对象或Map(下同)。
调用示例:
Navigator.of(context).pushNamed("/page1", arguments:'这里是参数');
// 或
Navigator.pushNamed(context, '/page01', arguments:'这里是参数');
上面示例表示保留当前路由栈新建page01,向page01页传递字符串参数“这里是参数”五个汉字。
- pushNamedAndRemoveUntil
根据条件删除当前路由栈中的路由,然后创建新页。
⑴ 使用 ModalRoute.withName() 参数:
从当前路由栈顶部(最近的)开始弹出(删除)路由,直到指定的页面为止,保留指定的页面。
调用示例:
Navigator.of(context).pushNamedAndRemoveUntil('/page02', ModalRoute.withName('/page01'), arguments:'这里是参数');
// 或
Navigator.pushNamedAndRemoveUntil(context, '/page02', ModalRoute.withName('/page01'), arguments:'这里是参数');
上面示例表示从当前页开始向前依次删除路由直到page01为止,保留page01,然后新建page02,向page02页传递字符串参数“这里是参数”五个汉字。
.
⑵ 使用 (Route route)=>false 参数:
将当前路由栈清空
调用示例:
Navigator.of(context).pushNamedAndRemoveUntil('/page02', (Route<dynamic> route)=>false, arguments:'这里是参数');
// 或
Navigator.pushNamedAndRemoveUntil(context, '/page02', (Route<dynamic> route)=>false, arguments:'这里是参数');
上面示例表示从将当前路由栈中所有路由删除,然后新建page02,向page02页传递字符串参数“这里是参数”五个汉字。
- pushReplacementNamed
在当前路由栈中删除当前路由,然后创建新页。也就是使用新建页的路由替换当前路由。此方法没有页面转场的效果。
调用示例:
Navigator.of(context).pushReplacementNamed('/page02', arguments:'这里是参数');
// 或
Navigator.pushReplacementNamed(context, '/page02', arguments:'这里是参数');
上面示例表示将当前路由栈中的当前页路由替换为新建的page02,向page02页传递字符串参数“这里是参数”五个汉字。没有转场效果
- popAndPushNamed
此方法与pushReplacementNamed在路由栈中最终结果相同,只是在进入新建页面时,有转场效果。
调用示例:
Navigator.of(context).popAndPushNamed('/page02', arguments:'这里是参数');
// 或
Navigator.popAndPushNamed(context, '/page02', arguments:'这里是参数');
上面示例表示将当前路由栈中的当前页路由替换为新建的page02,向page02页传递字符串参数“这里是参数”五个汉字。有转场效果
- popUntil
回到之前曾经打开的指定页,不重建不刷新页面,保持原页面状态,删除指定页之后的所有路由,不能携带参数。
调用示例:
Navigator.of(context).popUntil(ModalRoute.withName('/page02'));
// 或
Navigator.popUntil(context, ModalRoute.withName('/page02'));
上面示例表示回到曾经打开的page02,删除page02页之后的所有路由
.
MaterialApp路由相关属性配置要点
在MaterialApp中与本例相关的属性有 home、initialRoute、routes、onGenerateRoute 四个。
- home:指定应用启动后首次加载的页面,是类名,不是命名路由的页面名称。
- initialRoute:同 home,但是指定的是命名路由routes中的页面名,不是类名。
- routes:命名路由列表,是一个Map类型,页面名映射该页对应的实现类名。
- onGenerateRoute:命名路由拦截器,只有在routes中查不到页名时才会触发这个事件。
▲ 在 home 中指定的页面,该页在 routes 中的页面名称是不能压入路由栈的。
▲ 因 home 与 initialRoute 作用基本相同,所以不能同时使用,否则会产生冲突。
▲ 只有配置了 routes 或 启用了 onGenerateRoute 后,才可以使用 initialRoute,否则无处查找页面名会产生错误。
▲ 若配置了 routes、home 后,routes 列表中有写 ‘/’ 映射,将会产生冲突,因为命名路由规则中,’/’ 代表的是根页,也是初始化应用首先要加载的页面。所以配置了 routes 就尽量不要配置 home。
▲ 即使在 routes 列表中给实现类配置了参数,也是不能给实现类的初始化参数传递参数的。必须配置 onGenerateRoute 并在该事件的方法内实现参数传递才可以实现,否则只能在 build 内通过 ModalRoute.of(context).settings.arguments 获取传递的参数。
▲ 若希望每次通过命令路由创建页面都能走路由拦截器,那定义routes列表后,不要配置routes属性。由拦截器去加载routes列表,否则在routes列表存在新建页名的时候,路由拦截器是不会被触发的。
.
routes列表onGenerateRoute拦截器要点
通常为了方便,都是把这两个放在一个独立的文件内。
先看一下routes列表的定义,粘贴部分代码如下:
final routes = {
'/': (context) => Home(),
'/page01': (context) => Page01(),
'/page02': (context, {arguments}) => Page02(param:arguments),
};
▲ 上面代码中 ‘/’ 是应用初始化时,在没有配置 initialRoute(在MaterialApp中)和 home 时自动加载的页面,若配置了initialRoute则会加载initialRoute指定的页面。所以尽量我们把应用的默认加载页命名为 ‘/’,这更利于代码阅读与交流(其他每个页名前面的 / 也不是必需的)。
▲ 上面代码中的 {arguments} 和 param:arguments 是为了给页面实现类(如:Page02)的初始化参数传递参数而写的,arguments 这个名字不能更改,param 必须与 Page02 类中的接收参数名称一致。这个配置要最终实现参数传递到实现类初始化参数中,必须有 onGenerateRoute 拦截器配合才能实现。
▲ 如果只需要在 build 内通过 ModalRoute.of(context).settings.arguments 获取参数,则在这里不需要配置参数传递,像 ‘/’、’/page01’ 那样写即可。
再看一下onGenerateRoute拦截器的定义,粘贴示例代码如下:
Route<dynamic> onGenerateRoute(RouteSettings settings) {
String routeName = settings.name;
if (!routes.containsKey(routeName)) routeName = '/';
final Function pageContentBuilder = routes[routeName];
if (settings.arguments != null) {
return MaterialPageRoute(
builder: (context) => pageContentBuilder(context, arguments: settings.arguments),
settings: settings,
);
} else {
return MaterialPageRoute(
builder: (context) => pageContentBuilder(context),
settings: settings,
);
}
示例拦截器中,先获取路由名称(因在MaterialApp中没有配置routes,所以每次创建命名路由页面都会触发这里)给routeName,然后查询是否存在于routes的key中,如果不在则把routeName的值更改为 ‘/’,后面就会加载根页。这是为了处理代码意外,避免名称错误查不到而造成应用停止。
然后使用 routeName 到 routes 获取映射的实现类给 pageContentBuilder。
settings.arguments 是通过 pushNamed 等命名路由方法传递的参数,在这里通过这个获取。
接下来判断 settings.arguments 是否为 null,如果不是 null 说明有传递参数,那在 MaterialPageRoute 加载路由页面时就要给实现类传参,否则就不需要。
settings: settings 这句是重中之重, 大部分文章中都没有这句,这句是为了让通过拦截器创建的新页面也有路由名称,主要使用的是 settings.name 属性。如果新创建的页面没有给名字,后续的使用页面名称操作路由时,就可能会出现问题。
.
示例不同情况路由栈内容分析
以打开应用后,以从home页一路点击按钮到page04为前提,在示例中页面page04点击不同的按钮,对路由栈内的情况有如下影响(以数组格式代替垂直表示的栈图,数组左侧为栈底):
- page04呈现后的路由栈:
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’] - 点击按钮 A 后的路由栈(使用pushNamed):
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page04’, ‘/page02’] - 点击按钮 B 后的路由栈(使用pushNamedAndRemoveUntil):
[’/’, ‘/page01’, ‘/page02’] - 点击按钮 C 后的路由栈(使用pushNamedAndRemoveUntil):
[’/page02’] - 点击按钮 D 后的路由栈(使用pushReplacementNamed):
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page02’] - 点击按钮 C 后的路由栈(使用popAndPushNamed):
[’/’, ‘/page01’, ‘/page02’, ‘/page03’, ‘/page02’] - 点击按钮 D 后的路由栈(使用popUntil):
[’/’, ‘/page01’, ‘/page02’]
.
我的感悟
虽说天下文章一大抄,但是技术问题还是需要真正的实践才能知晓真实的结果。很多技术源于国外,国内资料多为业余翻译版。当遇到问题很难搞定的时候,别忘记放下资料深入实践一下,可能得到的结论更直接。更多可以尝试自行翻译外文官网的相关片段,或许可以得到不一样信息。这里还是要感谢翻译外文技术资料的朋友们,辛苦了!
希望本文能给相关的后来者一定的解惑,若对本文内容有异议,请以官方文档为准。