Flutter中提供了Navigator实现页面跳转功能,一个独立页面就是一个路由,因此称为路由导航。
有两种跳转方式:
- 通过路由直接跳转,就是说想要跳转到Page,那么直接将Page当作参数传递进去就可以了(类似于安卓的intent直接跳转Activity)。
- 通过命名路由参数跳转,就是说先将PageA注册到路由表内并起一个名字,然后其他页面通过这个名字进行跳转(类似于安卓的隐式跳转)。
Navigator API方法列表
通过路由直接跳转:
- push 栈顶入栈一个新的路由
- pushReplacement 将当前路由替换为一个新的路由
- pushAndRemoveUntil 根据参数决定栈内路由清除多少,并入栈一个新路由。
通过命名路由跳转:
- pushNamed 栈顶入栈一个新的路由
- pushReplacementNamed 将当前路由替换为一个新的路由
- pushNamedAndRemoveUntil 根据参数决定栈内路由清除多少,并入栈一个新路由
路由主动出栈:
- pop 当前路由出栈
- popAndPushNamed 当前路由出栈,入栈一个新路由
- popUntil 从栈顶开始,逐个pop,直到参数中指定的路由为止
- canPop 判断当前路由是否可以出栈,如果栈内只有当前路由,返回false,如果当前路由前面还有路由,返回true。也就是说栈底的路由,该方法返回false
- maybePop 当前路由如能能出栈就出栈,如果不能就什么都不做。
路由删除:
- removeRoute
- removeRouteBelow
路由替换:
- replace
- replaceRouteBelow
路由判断方法:
- of 获取当前context 的Navigator的实例.
- 几乎所有的Navigtor方法实现都是调用of后,再调用具体方法的,如pop:
static bool pop<T extends Object>(BuildContext context, [ T result ]) {
return Navigator.of(context).pop<T>(result);
}
通过路由直接跳转
Navigator.push(BuildContext context, Route route)
描述:当前页面不变,跳转到一个新的页面。也就是栈顶入栈一个新的路由。 路由关系:
- 当前路由顺序为A-B-C
- 从C push D
- 路由顺序为A-B-C-D
- 如果在D页面,pop了,那么路由顺序为A-B-C
页面间传递参数:
- 通过目标页面的构造方法传递参数
- 可以从目标页面接收回传的参数
示例:页面跳转,传递参数给下一个页面,并获取下一个页面关闭后返回给当前页面的参数。 当前页面:
RaisedButton(
onPressed: () {
// 接收一个Future的返回值,返回值是一个数组
var result = Navigator.push(context, MaterialPageRoute(
builder: (BuildContext context){
return PageA("这是传递给PageA的参数");// 通过构造方法传递参数
}
));
// 获取返回值
result.then((response){
this.setState((){
text = response[0];
});
});
},
child: Text("页面跳转至PageA,传递参数,并接收页面返回值"),
),
PageA:
RaisedButton(
child: Text("向上个页面回传值"),
onPressed: (){
Navigator.pop(context,["来自PageA的回传参数"]);// 往回传一个数组
// Navigator.pop(context,"来自PageA的回传参数");// 往回传一个字符串
},
)
Navigator.pushReplacement(BuildContext context, Route route, { TO result })
路由关系:
- 当前路由顺序为A-B-C
- 从C pushReplacement D
- 路由顺序为A-B-D
- 如果在D页面pop了,那么路由顺序为A-B
参数关系:
- 通过目标页面的构造方法传递参数
- 页面已经不存在,无法从目标页面接收回传的参数
RaisedButton(
onPressed: () async {
// 接收一个Future的返回值,返回值是一个数组
var result = Navigator.pushReplacement(context, MaterialPageRoute(
builder: (BuildContext context){
return PageA("这是传递给PageA的参数");
}
));
// 获取返回值
result.then((response){
print("response===$response");
// 页面已经出栈,不可再次调用
// this.setState((){
// text = response[0];
// });
});
},
child: Text("当前页面替换为PageA,可以传递参数,但无法接收页面返回值"),
Navigator.pushAndRemoveUntil((BuildContext context, Route newRoute, RoutePredicate predicate)
描述:打开一个新的页面,并根据predicate的值的情况来决定如何处理新页面之前的路由,predicate值得情况分三种:
- 如果给predicate传递一个方法,方法的返回值是false,那么删除新页面之前的所有路由。
- 当前路由顺序为A-B-C-D-E
- 从E pushAndRemoveUntil 页面F,并且predicate返回false
- 路由顺序为F,A—E 全部出栈了。
- 如果在F页面pop了,那么app回到桌面。
- 如果predicate传递一个方法,方法的返回值为true,那么新页面之前的路由保持不变。
- 当前路由顺序为A-B-C-D-E
- 从E pushAndRemoveUntil 页面F,并且predicate返回true
- 路由顺序为A-B-C-D-E-F,A—E 全部不变。
- 如果在F页面pop了,那么路由为:A-B-C-D-E。
- 如果predicate的值是ModalRoute.withName指定的一个命名路由名称,那么新页面和该指定的路由名称之间的路由全部删除。 路由关系:
- 当前路由顺序为A-B-C-D-E
- 从E pushAndRemoveUntil 页面F,并且predicate的值是ModalRoute.withName('C')。(此处有坑,后面再讲)
- 路由顺序为A-B-C-F,D 、E 出栈。
- 如果在F页面pop了,那么路由顺序为:A-B-C。
参数关系:
- 通过目标页面的构造方法传递参数
- 页面已经不存在,无法从目标页面接收回传的参数
示例,给predicate传入一个方法,返回值是false或true
RaisedButton(
onPressed: () async {
// 接收一个Future的返回值,返回值是一个数组
var result = Navigator.pushAndRemoveUntil(context,
MaterialPageRoute(builder: (BuildContext context) {
return PageA("这是传递给PageA的参数");
}),
// predicate参数
(route) {
print('route=$route}');
// return false;// PageA之前的路由全部删除
return true; // PageA之前的路由保持不变
});
// 获取返回值
result.then((response) {
// 页面已经出栈,不可再次调用
// this.setState((){
// text = response[0];
// });
});
},
示例,给predicate参数传递ModalRoute.withName,实现步骤:
- 假设有T-A-B-C-D五个路由
- 在T页面push到A,需要设置settings: RouteSettings(name:"pagea"),相当于给A打了一个标签“pagea”,如下:
Navigator.push(context,
MaterialPageRoute(settings: RouteSettings(name:"pagea"),builder: (BuildContext context) {
return PageA("这是传递给PageA的参数");
}));
- 然后A-push-B-push-C,此时路由顺序为:T-A-B-C
- 在C页面 pushAndRemoveUntil 到D页面,同时指定ModalRoute.withName("pagea")。
Navigator.pushAndRemoveUntil(context,
MaterialPageRoute(builder: (BuildContext context) {
return PageD("这是传递给PageD的参数");
}),
ModalRoute.withName('pagea')
);
- 此时路由顺序为:T-A-D。
- 最重要的是settings: RouteSettings(name:"pagea") ,ModalRoute.withName(name)中的参数name,指的是注册的路由名字,也可以是RouteSetting设定的那么,如果既注册路由了又RouteSetting了,那么RouteSetting的优先。
通过命名路由跳转
首先需要将路由进行注册
在MaterialApp内注册路由
class MainPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Good'),
routes: {
"mainpage": (context) => MainPage(),
"navigatortest": (context) => NavigatorTest(),
"pagea": (context) => PageA("aaa"),
"pageb": (context) => PageB("bbb"),
"pagec": (context) => PageC("cc"),
"paged": (context) => PageD("dd"),
},
);
}
}
路由跳转
- Navigator.pushNamed(context, "paged", arguments: "111");//跳转到paged,并传递参数
- Navigator.pushReplacementNamed(context, "paged",arguments: "111");//用paged替换掉当前路由,并传递参数
- Navigator.pushNamedAndRemoveUntil(context, "paged", (route) { // return true; return false; }, arguments: "111");//跳转到paged,并根据第三个参数predicate的返回值决定新路由之前的路由的处理方式,为true保持不变,为false则新路由外的所有路由都pop掉。
Navigator.pushNamed(BuildContext context, String routeName, {Object arguments})
描述:与push方法作用一样,从当前页面打开一个新的页面,栈顶入栈一个路由,当前路由不变。
路由关系:
- 当前路由顺序为A-B-C
- 从C push D
- 路由顺序为A-B-C-D
- 如果在D页面,pop了,那么路由顺序为A-B-C
参数传递:
- 在调用pushNamed方法时,有个可选命名参数arguments,通过此参数传递给下个页面数据。
- 下个页面在build方法内获取参数,通过ModalRoute.of(context).settings.arguments接收到参数。
- pushNamed方法的返回值也是一个Future,可以接收下个页面返回的数据。
RaisedButton(
onPressed: () {
var result = Navigator.pushNamed(context, "pagea", arguments: "111");
result.then((response){
// 下个页面的返回值
});
},
child: Text("pushNamed 打开新页面,传递参数和接收返回值"),
),
新路由接收参数 在build方法内获取参数,var args=ModalRoute.of(context).settings.arguments;
class _PageDState extends State<PageD> {
@override
Widget build(BuildContext context) {
// 接受参数
var args=ModalRoute.of(context).settings.arguments;
return Scaffold(
appBar: AppBar(
title: Text("PageD"),
centerTitle: true,
),
body: Center(,
);
}
}
Navigator.pushReplacementName()
Navigator.pushReplacementNamed 与Navigator.pushReplacement唯一区别是用注册的路由命名替换了new PageA()式的直接路由。其他使用方法是一样的
Navigator.pushNamedAndRemoveUntil()
Navigator.pushNamedAndRemoveUntil 与Navigator.pushAndRemoveUntil唯一区别是用注册的路由命名替换了new PageA()式的直接路由。其他使用方法是一样的
路由出栈
Navigator.pop(BuildContext context, [ T result ])
描述:关闭当前页面,并将参数传递给来时的页面。也就是,当前路由从栈顶出栈。
- 当前路由顺序为A-B-C
- 在C页面pop,那么路由为:A-B
- 在pop时可以向来时的路由页面传递参数。
RaisedButton(
child: Text("向上个页面回传值"),
onPressed: (){
Navigator.pop(context,result);// 任何类型的参数
},
)
bool canPop(BuildContext context)
描述:判断当前路由是否可以出栈,如果栈内只有当前路由,返回false,如果当前路由前面还有路由,返回true。也就是说栈底的路由,该方法返回false。
if(Navigator.canPop(context)){
Navigator.pop(context);
}
如果canPop返回false,但是执行了pop方法,那么就会报错。
Navigator.maybePop(BuildContext context, [ T result ])
描述: 当前路由如能能出栈就出栈,如果不能就什么都不做。
- 是pop的安全返回方式.
- 参数作用于pop的一样。
Navigator.maybePop(context);
- 相当于对pop外层加了一层canpop判断,等同于:
if(Navigator.canPop(context)){
Navigator.pop(context);
}
Navigator.popAndPushNamed<T extends Object, TO extends Object>(BuildContext context,String routeName, {TO result,Object arguments,})
描述:当前路由出栈,栈顶入栈一个新路由。
路由顺序:
- 当前路由顺序为:A-B-C
- 在C popAndPushNamed 路由D
- 那么路由顺序为: A-B-D
RaisedButton(
onPressed: () {
Navigator.popAndPushNamed(context, "paged");
},
child: Text("popAndPushNamed"),
),
Navigator.popUntil(BuildContext context, RoutePredicate predicate)
描述:栈顶路由逐个出栈,直至predicate指定的条件。 路由顺序:
- 当前路由顺序为A-B-C-D-E
- 在E popUntil(context,Module.withName('B')),withName内的名字可以是为路由注册的名字,或是为路由设置的RouteSetting。
- 那么路由顺序为:A-B
RaisedButton(
onPressed: () {
Navigator.popUntil(context,ModalRoute.withName('pagea'));
},
child: Text("popUntil"),
),
路由删除和替换
路由的删除和替换,通常是对站内已经存在的路由的处理,因此,想要删除一个路由,需要拿到对该路由的引用。 建立一个Map保存那些已经存在的路由:
class RouteMap {
static Map<String,MaterialPageRoute> map = new Map();
// 保存路由
static addRoute(String key,MaterialPageRoute route){
map[key]=route;
}
// 获取指定的路由
static MaterialPageRoute getRoute(String key){
return map[key];
}
}
每次跳转到新的路由时,都将新的路由的引用保存在RouteMap中,如:
RaisedButton(
onPressed: (){
MaterialPageRoute route = MaterialPageRoute(builder: (BuildContext context) {
return PageA("这是传递给PageA的参数");
});
RouteMap.addRoute("pagea", route);// 保存路由
Navigator.push(context,route);
},
child: Text("add Route"),
),
我们假设路由的跳转顺序为:A-B-C-D-E-F 那么RouteMap中的值为:
RouteMap.addRoute("pageb", routeb);
RouteMap.addRoute("pagec", routec);
RouteMap.addRoute("paged", routed);
RouteMap.addRoute("pagee", routee);
RouteMap.addRoute("pagef", routef);
Navigator.removeRoute(BuildContext context, Route route)
描述:删除一个指定的路由,如果该路由存在于栈内正常删除,否则报错 路由顺序:
- 假设当前路由顺序为:A-B-C-D-E-F
- 在F页面,Navigator.removeRoute(context,RouteMap.getRoute("paged"));
- 路由顺序为:A-B-C-E-F
RaisedButton(
onPressed: () {
Navigator.removeRoute(context,RouteMap.getRoute("pagea"));
},
child: Text("removeRoute"),
),
Navigator.removeRouteBelow(BuildContext context, Route anchorRoute)
描述:给定一个路由,在路由表内删除其前一个路由
- 假设当前路由顺序为:A-B-C-D-E-F
- 在F页面,removeRouteBelow(context,RouteMap.getRoute("paged"));
- 路由顺序为:A-B-D-E-F
RaisedButton(
onPressed: () {
Navigator.removeRouteBelow(context,RouteMap.getRoute("paged"));
},
child: Text("removeRouteBelow"),
),
Navigator.replace(BuildContext context, { @required Route oldRoute, @required Route newRoute })
描述:用一个新的路由,将路由表内的一个已存在的路由替换掉
- 假设当前路由顺序为:A-B-C-D-E
- 在E页面,replace(context,oldRoute:RouteMap.getRoute("pageb"),newRoute:MaterialPageRoute(PageF());
- 路由顺序为:A-F-C-D-E
RaisedButton(
onPressed: () {
Navigator.replace(context, oldRoute: RouteMap.getRoute("pageb"), newRoute: MaterialPageRoute(builder: (BuildContext context){
return PageF("dd");
}));
},
child: Text("replace"),
),
Navigator.replaceRouteBelow(BuildContext context, { @required Route anchorRoute, Route newRoute })
描述:用一个新的路由,将路由表内的一个已存在的路由的前面的路由给替换掉
- 假设当前路由顺序为:A-B-C-D-E
- 在E页面,replace(context,oldRoute:RouteMap.getRoute("pagec"),newRoute:MaterialPageRoute(PageF());
- 路由顺序为:A-F-C-D-E
RaisedButton(
onPressed: () {
Navigator.replaceRouteBelow(context, anchorRoute: RouteMap.getRoute("pagec"),newRoute: MaterialPageRoute(builder: (BuildContext context){
return PageF("dd");
}));
},
child: Text("replaceRouteBelow"),
),
通过路由导航与通过命名路由导航的异同
路由导航:
- 直接通过类名跳转,跳转动作分散在各个类的内部。
- 界面间传递数据必须放在目标类的构造方法内。 命名路由:
- 所有路由都放到一个地方进行注册,一目了然。
- 界面间传递数据是通过方法参数传递过去的。
- 如果在业务场景中必须实时创建一些类的路由,构造方法内的数据是业务关联的,那么命名路由就不太合适了。
因此根据自己的实用场景进行选择,最好不要混用,而是只选择一种使用,我个人倾向于使用命名路由,方便管理。