1. 基本介绍
Route 在 Flutter 里是极其重要的部分,用来处理页面跳转。本文主要普通路由,命名路由,以及自定义路由等。
如果是简单的页面跳转可以参考Flutter 页面创建与跳转。
2. 示例代码
代码下载地址。如果对你有帮助的话记得给个关注,代码会根据我的 Flutter 专题不断更新。
3. 基础功能
- 命名路由 routes
- 路由跳转 push、pop
- 初始路由 initialRoute
- 路由拦截 onGenerateRoute
4. 命名 Route 详解
4.1 容器创建
优雅的编程,我们创建一个 materialapp.dart 文件。
import 'package:flutter/material.dart';
class FMMaterialAppVC extends StatelessWidget{
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
home: Scaffold(
body: AAA(),
),
routes: {
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
);
}
}
class AAA extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('AAA'),
),
body: Center(
child: RaisedButton(
child: Text('点击前往BBB'),
onPressed: (){
Navigator.pushNamed(context, '/bbb');
},
),
),
);
}
}
class BBB extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('BBB'),
),
body: Center(
child: RaisedButton(
child: Text('点击前往CCC'),
onPressed: (){
Navigator.pushNamed(context, '/ccc');
},
),
),
);
}
}
class CCC extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('CCC'),
),
body: Center(
child: RaisedButton(
child: Text('点击前往DDD'),
onPressed: (){
Navigator.pushNamed(context, '/ddd');
},
),
),
);
}
}
class DDD extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('DDD'),
),
body: Center(
child: RaisedButton(
child: Text('点击回到AAA'),
onPressed: (){
Navigator.popUntil(context, (route) => route.isFirst);
},
),
),
);
}
}
我们为 BBB,CCC,DDD 进行了命名,效果如下。
4.2 路由跳转方式之 Push 详解
在上文中,我们使用了 pushNamed 方法,推到一个新的页面。
class AAA extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('AAA'),
),
body: Center(
child: RaisedButton(
child: Text('点击前往BBB'),
onPressed: (){
Navigator.pushNamed(context, '/bbb');
},
),
),
);
}
}
路由 push 其实还有很多种写法。
4.2.1 Navigator.pushNamed
无参数
Navigator.pushNamed(context, '/bbb');
有参数
final datas = {"data": ["1","2","3"]};
Navigator.pushNamed(context, '/bbb', arguments: datas);
4.2.2 Navigator.push
无参数
Navigator.push(
context,
MaterialPageRoute(
builder: (context){
return BBB();
}
);
有参数,name 可以用来给路由命名,arguments 用来传递参数
final datas = {"data": ["1","2","3"]};
Navigator.push(
context,
MaterialPageRoute(
builder: (context){
return BBB();
},
settings: RouteSettings(
name: '/bbb',
arguments: datas,
),
),
);
上面代码意思就是跳转到 BBB() 页面,并且给 BBB() 命名为 '/bbb',在路由堆栈中,读取到这个 route 时,route.setting.name 与该命名相同。
4.2.3 Navigator.of(context).pushNamed
无参数
Navigator.of(context).pushNamed('/bbb');
有参数
final datas = {"data": ["1","2","3"]};
Navigator.of(context).pushNamed('/bbb',arguments: datas);
4.2.4 Navigator.of(context).push
无参数
Navigator.of(context).push(
MaterialPageRoute(
builder: (context){
return BBB();
}
),
);
有参数
final datas = {"data": ["1","2","3"]};
Navigator.of(context).push(
MaterialPageRoute(
builder: (context){
return BBB();
},
settings: RouteSettings(
name: '/bbb',
arguments: datas,
),
),
);
4.3 路由跳转方式之 Push 进阶
4.2 中是最为常用的页面进出栈,但是在某些场合需要有特殊的处理。
注意:写法大同小异,后续就不在赘述传参方式,均与4.2相同。
4.3.1 pushNamedAndRemoveUntil
例如我们页面从AAA->BBB->CCC->DDD,这样进行页面,但是我们希望 CCC 再使用过后就被销毁。使得页面层级变为 AAA-BBB-DDD。
- Navigator.pushNamedAndRemoveUntil
如下方代码,我们在 CCC 中来做处理。
我们的页面层级为 AAA->BBB->CCC->DDD,我们在 CCC 中 push 到 DDD 页面,然后从栈顶开始删除,直到这个route.setting.name == '/bbb',也就是销毁 BBB 与 DDD 中间所有的页面。
class CCC extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('CCC'),
),
body: _listView(context),
);
}
ListView _listView(BuildContext context){
return ListView(
children: [
ListTile(
title: Text("Navigator.pushNamed"),
onTap: (){
Navigator.pushNamed(context, '/ddd');
},
),
ListTile(
title: Text("Navigator.pushNamedAndRemoveUntil"),
onTap: (){
Navigator.pushNamed(context, '/ddd');
},
),
ListTile(
title: Text("Navigator.of(context).pushNamedAndRemoveUntil"),
onTap: (){
Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.settings.name == '/ddd');
},
),
ListTile(
title: Text("Navigator.pushNamedAndRemoveUntil - current"),
onTap: (){
Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.isCurrent);
},
),
ListTile(
title: Text("Navigator.pushReplacementNamed"),
onTap: (){
Navigator.pushReplacementNamed(context, '/ddd');
},
),
ListTile(
title: Text("Navigator.of(context).pushReplacementNamed"),
onTap: (){
Navigator.of(context).pushReplacementNamed('/ddd');
},
),
ListTile(
title: Text("Navigator.pushReplacement"),
onTap: (){
Navigator.pushReplacement(context,
MaterialPageRoute(
builder: (context){
return DDD();
},
settings: RouteSettings(
name: '/ddd',
),
),
);
},
),
ListTile(
title: Text("Navigator.of(context).pushReplacement"),
onTap: (){
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context){
return DDD();
},
settings: RouteSettings(
name: '/ddd',
),
),
);
},
),
ListTile(
title: Text("Navigator.popAndPushNamed"),
onTap: (){
Navigator.popAndPushNamed(context, '/ddd');
},
),
ListTile(
title: Text("Navigator.of(context).popAndPushNamed"),
onTap: (){
Navigator.of(context).popAndPushNamed('/ddd');
},
),
],
);
}
}
如gif图,我们push 顺序为 AAA->BBB->CCC->DDD,返回时为 DDD->BBB->AAA,CCC页面销毁。
- Navigator.of(context).pushNamedAndRemoveUntil
Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.settings.name == '/bbb');
- Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.isCurrent)
跳转到指定页面,并删除前边所有页面
Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.isCurrent)
// 等价于下面这行代码
//Navigator.of(context).pushNamedAndRemoveUntil('/ddd', (route) => route.settings.name == '/ddd');
4.3.2 pushReplacementNamed
例如我们页面从AAA->BBB->CCC->DDD,这样进行页面,但是我们希望 CCC 再使用过后就被销毁。使得页面层级变为 AAA-BBB-DDD。
注意:同4.2.2区别,假设我们的页面为 AAA->BBB->CCC->DDD->EEE->FFF,我们在 EEE 页面跳转到 FFF 并且 removeUntil('/bbb'),那么我们的页面层级则会变成 AAA->BBB->FFF。而使用 pushReplacementNamed('/ddd'),则会变成 AAA->BBB->CCC->DDD->FFF。
- Navigator.pushReplacementNamed
Navigator.pushReplacementNamed(context, '/ddd');
Navigator.of(context).pushReplacementNamed
Navigator.of(context).pushReplacementNamed('/ddd');
- Navigator.pushReplacement
具体可以参考 4.2 写法
Navigator.pushReplacement(context,
MaterialPageRoute(
builder: (context){
return DDD();
},
settings: RouteSettings(
name: '/ddd',
),
),
);
- Navigator.of(context).pushReplacement
具体可以参考 4.2 写法
Navigator.of(context).pushReplacement(
MaterialPageRoute(
builder: (context){
return DDD();
},
settings: RouteSettings(
name: '/ddd',
),
),
);
4.3.3 popAndPushNamed
先 pop 一级,然后在 push 到对应页面,效果与4.3.2 相同
- Navigator.popAndPushNamed
Navigator.popAndPushNamed(context, '/ddd');
- Navigator.of(context).popAndPushNamed
Navigator.of(context).popAndPushNamed('/ddd');
4.4 路由跳转方式之 Pop 详解
Pop 是路由返回方式,与 push 正好相反,push 用来控制页面入栈,而 pop 则控制页面出栈。其实左上角的返回按钮会默认执行一次 pop 方法。
由于 pop 是返回方式,我们在最后一个页面 DDD 中操作。
class DDD extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('DDD'),
),
body: _listView(context),
);
}
ListView _listView(BuildContext context){
return ListView(
children: [
ListTile(
title: Text('Navigator.pop'),
onTap: (){
Navigator.pop(context);
// final data = {"data":["1","2","3"]};
// Navigator.pop(context, data);
},
),
ListTile(
title: Text('Navigator.pop'),
onTap: (){
Navigator.of(context).pop();
},
),
ListTile(
title: Text('Navigator.canPop'),
onTap: (){
bool canpop = Navigator.canPop(context);
if (canpop) Navigator.pop(context);
},
),
ListTile(
title: Text('Navigator.maybePop'),
onTap: (){
Navigator.maybePop(context);
},
),
],
);
}
}
4.4.1 Navigator.pop
无参数
Navigator.pop(context);
有参数
final data = {"data":["1","2","3"]};
Navigator.pop(context, data);
4.4.2 Navigator.of(context).pop
无参数
Navigator.of(context).pop();
有参数
final data = {"data":["1","2","3"]};
Navigator.of(context).pop(data);
4.4.3 Navigator.canPop
判断是否可以 pop
bool canpop = Navigator.canPop(context);
if (canpop) Navigator.pop(context);
4.4.4 Navigator.of(context).canPop
bool canpop = Navigator.of(context).canPop();
4.4.5 Navigator.maybePop
先判断是否可以 pop,如果可以,在pop,相当于 4.4.3 中代码
Navigator.maybePop(context);
4.4.6 Navigator.of(context).maybePop
Navigator.of(context).maybePop(context);
4.5 路由跳转方式之 Pop 进阶
4.4 中是最为常用的页面进出栈,但是在某些场合需要有特殊的处理。pop 进阶方法比较少,只有 popUntil 与 popAndPushNamed ,在4.3.3中讲过后者了,这里不重复叙述。
- 回到栈顶
Navigator.popUntil(context, (route) => route.isFirst);
- 回到指定页面
我们使用下方代码回到 BBB 页面
Navigator.popUntil(context, (route) => route.settings.name == '/bbb');
- 页面堆栈了解
这里我们自定义一下方法,查看一下当前堆栈的所有 route
Navigator.popUntil(context, (route) => _lookRoutes(route));
bool _lookRoutes(Route route){
print(route);
return route.isFirst;
}
我们来看一下上述页面的所有效果。
5. 初始路由 initialRoute
我们注释掉 home 属性,使用 initialRoute 来加载页面
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
// home: Scaffold(
// body: AAA(),
// ),
initialRoute: '/ccc',
routes: {
'/aaa': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
);
}
5.1 initialRoute 使用了未命名的路由
报错如下。
A GlobalKey was used multiple times inside one widget's child list.
注意:initialRoute 需要使用 routes 表中的命名过的路由,否则会报错。
5.2 解决方案
a. 更改 initialRoute 为 routes 表里命名过得路由。
b. 设置根路由
设置路由 '/' ,当初始路由异常时,会停留在根路由。
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
// home: Scaffold(
// body: AAA(),
// ),
initialRoute: '/ccc',
routes: {
'/': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
);
}
- c. 使用未知路由
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
// home: Scaffold(
// body: AAA(),
// ),
initialRoute: '/cc',
routes: {
'/aaa': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
onUnknownRoute: (setting){
print(setting);
return MaterialPageRoute(builder: (context) => AAA());
},
);
}
6. 未知路由 onUnknownRoute
class FMMaterialAppVC extends StatelessWidget{
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
initialRoute: '/',
routes: {
'/': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
onUnknownRoute: (setting){
print(setting);
return MaterialPageRoute(builder: (context) => AAA());
},
);
}
}
class BBB extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('BBB'),
),
body: Center(
child: RaisedButton(
child: Text('点击前往CCC'),
onPressed: (){
Navigator.pushNamed(context, '/cc');
},
),
),
);
}
}
我们在 BBB 页面中 push 一个错误的 '/cc' 路由,按照我们 onUnknownRoute 中的设置,我们返回一个 AAA(),用这个可以做一个统一的报错页面。
7. 路由拦截 onGenerateRoute
class FMMaterialAppVC extends StatelessWidget{
@override
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
// home: Scaffold(
// body: AAA(),
// ),
initialRoute: '/',
routes: {
'/': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
},
// onGenerateInitialRoutes: (string){
// return [
// MaterialPageRoute(builder: (context) => AAA()),
// ];
// },
onGenerateRoute: (setting){
print(setting);
return MaterialPageRoute(builder: (context) => CCC());
},
onUnknownRoute: (setting){
print(setting);
return MaterialPageRoute(builder: (context) => AAA());
},
);
}
}
应该有很多小伙伴跟我一样,onGenerateRoute 不执行,明明写了 onGenerateRoute 方法,但是却不响应。查了挺多博客,没有相关描述,然后去翻了官方文档和源码,终于找到问题。
onGenerateRoute 路由拦截不能与命名路由一起使用,否则会只执行命名路由,不在进行拦截。下面我们注释掉命名路由,然后通过路由拦截,自定义一套路由跳转,从而实现命名路由的功能。
Widget build(BuildContext context) {
// TODO: implement build
return MaterialApp(
initialRoute: '/',
// routes: {
// '/': (context) => AAA(),
// '/bbb': (context) => BBB(),
// '/ccc': (context) => CCC(),
// '/ddd': (context) => DDD(),
// },
// onGenerateInitialRoutes: (string){
// return [
// MaterialPageRoute(builder: (context) => AAA()),
// ];
// },
onGenerateRoute: (setting){
print(setting);
final isLogin = true;
final routes = {
'/': (context) => AAA(),
'/bbb': (context) => BBB(),
'/ccc': (context) => CCC(),
'/ddd': (context) => DDD(),
};
if (!isLogin) {
return MaterialPageRoute(builder: (context) => AAA());
}
return MaterialPageRoute(builder: routes[setting.name], settings: setting);
},
onUnknownRoute: (setting){
print(setting);
return MaterialPageRoute(builder: (context) => AAA());
},
);
}
举一反三,在拦截路由中自定义一张路由表。当未登录时,对页面进行拦截,跳转到登录页面,已登录时,正常加载路由表内的页面,实现路由拦截功能。
8. 路由传值
在App中,页面之间的通讯和传值是非常重要的,这里只单独介绍一下使用路由时的传值方式,以及反向传值的方法。
我们在 BBB 页面使用 pushNamed 传值给 CCC,在 CCC 页面介绍如何接收路由传值。然后在 CCC 页面使用 pop 带参数回来,在 BBB 页面介绍如何接收路由反参。
8.1 路由正向传值以及反参接收
BBB 页面 push 到 CCC,并等待 CCC 回来时的反参。
class BBB extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('BBB'),
),
body: Center(
child: RaisedButton(
child: Text('点击前往CCC'),
onPressed: (){
var backValueFromDDD = Navigator.pushNamed(context, '/ccc',arguments: {'value': "我是BBB页面传过来的值"});
backValueFromDDD.then((value){
print("CCC 传回来反参了,${value}");
});
// Navigator.pushNamedAndRemoveUntil(context, '/ccc', (route) => route.isCurrent);
},
),
),
);
}
}
我们使用 then 属性来接收下一个页面 pop 回来带的参数。
8.2 路由接收正向传值以及反向传参
我们在 CCC 类中增加以下代码。
Widget build(BuildContext context) {
// TODO: implement build
var value = ModalRoute.of(context).settings.arguments;
print("BBB 页面带过来参数了,${value}");
return Scaffold(
appBar: AppBar(
title: Text('CCC'),
),
body: _listView(context),
);
}
ListTile(
title: Text("Navigator.of(context).pop 传参"),
onTap: (){
Navigator.of(context).pop({"value":"我是CCC页面带回来的值"});
},
),
8.3 打印效果
8.4 其他写法
在 BBB 中可以尝试以下写法,可以达到相同效果。
class BBB extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return Scaffold(
appBar: AppBar(
title: Text('BBB'),
),
body: Center(
child: RaisedButton(
child: Text('点击前往CCC'),
onPressed: () async {
var backValueFromDDD = await Navigator.pushNamed(context, '/ccc',arguments: {'value': "我是BBB页面传过来的值"});
backValueFromDDD;
print(backValueFromDDD);
// Navigator.pushNamedAndRemoveUntil(context, '/ccc', (route) => route.isCurrent);
},
),
),
);
}
}
9. 技术小结
- 路由在App中是一个非常重要的功能,我已经从事App开发多年了,对页面堆栈以及原理比较熟悉,所以上手起来比较容易。初学者需要更多耐心来理解和尝试路由的各个方法。
- 路由这一篇章花费时间较多,踩坑也比较多,为了尽可能的覆盖到路由的全部用法,本文篇幅也比较长。