Flutter之路由详解
本文主要包含两个方面:【路由导航】和【路由传值】
路由传值
Flutter中管理多个页面时有两个核心概念和类:Route
和Navigator
。
一个route
是一个屏幕或页面的抽象,Navigator
是管理route
的Widget
。Navigator
可以通过route
入栈和出栈来实现页面之间的跳转。
路由一般分为静态路由(即命名路由)和动态路由。
静态路由(即命名路由)
静态路由在通过Navigator
跳转之前,需要在MaterialApp
组件内显式声明路由的名称,而一旦声明,路由的跳转方式就固定了。通过在MaterialApp
内的routes
属性进行显式声明路由的定义。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: "/", // 默认加载的界面,这里为RootPage
routes: { // 显式声明路由
// "/":(context) => RootPage(),
"/A":(context) => Apage(),
"/B":(context) => Bpage(),
"/C":(context) => Cpage(),
},
home: RootPage(),
);
}
}
注意:如果指定了home属性,routes表则不能再包含此属性。
如上代码中【home: RootPage()】 和 【"/":(context) => RootPage()】两则不能同时存在。
例如:RootPage
跳转Apage
即:RootPage
—>Apage
Navigator.of(context).pushNamed("/A");
一般方法中带有Name
多数是通过静态路由完成跳转的,如pushNamed
、pushReplacementNamed
、pushNamedAndRemoveUntil
等。
动态路由
动态路由无需在MaterialApp
内的routes
中注册即可直接使用:RootPage —> Apage
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => Apage(),
));
动态路由中,需要传入一个Route
,这里使用的是MaterialPageRoute
,它可以使用和平台风格一致的路由切换动画,在iOS上左右滑动切换,Android上会上下滑动切换。也可以使用CupertinoPageRoute
实现全平台的左右滑动切换。
当然也可以自定义路由切换动画,使用PageRouteBuilder
:使用FadeTransition
做一个渐入过渡动画。
Navigator.of(context).push(
PageRouteBuilder(
transitionDuration: Duration(milliseconds: 250), // //动画时间为0.25秒
pageBuilder: (BuildContext context,Animation animation,
Animation secondaryAnimation){
return FadeTransition( //渐隐渐入过渡动画
opacity: animation,
child: Apage()
);
}
)
);
到现在为止,可能对路由有了一定的认识,,下面就结合具体方法来详细说明。
在这之前有必要说明:
Navigator.of(context).push
和Navigator.push
两着并没有特别的区别,看源码也得知,后者其实就是调用了前者。
of
:获取Navigator
当前已经实例的状态。
pop
返回当前路由栈的上一个界面。
Navigator.pop(context);
push / pushNamed :
见上,两者运行效果相同,只是调用不同,都是将一个page
压入路由栈中。直白点就是push
是把界面直接放入,pushNames
是通过路由名的方式,通过router使界面进入对应的栈中。
结果:直接在原来的路由栈上添加一个新的 page
。
pushReplacement / pushReplacementNamed / popAndPushNamed
替换路由,顾名思义替换当前的路由。
例如
Replacement.png
由图可知在BPage
使用替换跳转到Cpage
的时候,Bpage
被Cpage
替换了在堆栈中的位置而移除栈,CPage
默认返回的是APage
。
pushReplacement 使用的动态路由方式跳转:
Navigator.of(context).pushReplacement(MaterialPageRoute(
builder: (context) => Cpage(),
));
pushReplacementNamed 使用的静态路由方式,
Navigator.of(context).pushReplacementNamed("/C");
两者运行效果相同。
popAndPushNamed:
Navigator.of(context).popAndPushNamed("/C");
其实和上面两个方法运行的结果也是一致,区别就是动画效果不一样:BPage
—>CPage
的时候,CPage
会同时有pop
的转场效果和从BPage
页push
的转场效果。简单来说就是CPage
先pop
到BPage
,在push
到CPage
。(不知道是不是卡顿的原因,笔者看起来区别不大)
综上:3中方法结果一样,只是调用方式和过渡动画的区别,开发者自行选择。
pushAndRemoveUntil / pushNamedAndRemoveUntil
在使用上述方式跳转时,会按次序移除其他的路由,直到遇到被标记的路由(predicate
函数返回了true
)时停止。若 没有标记的路由,则移除全部。
当路由栈中存在重复的标记路由时,默认移除到最近的一个停止。
第一种
// 移除全部
Navigator.pushAndRemoveUntil(context,
MaterialPageRoute(builder: (_) => CPage()), (Route router) => router == null);
或
// 移除全部
Navigator.of(context).pushNamedAndRemoveUntil("/C", (Route router) => router == null);
此时的路由栈示意图:
RemoveUntil_all.png
可知出了要push
的CPage
,当前路由栈中所有的路由都被移除,CPage
变成根路由。
第二种:移除到RootPage停止
// "/"即为RootPage,标记后,移除到该路由停止移除
Navigator.pushAndRemoveUntil(context,
MaterialPageRoute(builder: (_) => CPage()), ModalRoute.withName('/'))
或
Navigator.pushAndRemoveUntil(context,
MaterialPageRoute(builder: (_) => CPage()), (Route router) => router.settings.name == "/");
// 只是写法不一样
或
Navigator.of(context).pushNamedAndRemoveUntil("/C", (Route router) => router.settings.name == "/");
或
Navigator.of(context).pushNamedAndRemoveUntil("/C", ModalRoute.withName("/"));
此时的路由栈示意图:
RemoveUntil_until.png
push
到CPage
的时候,移除到RootPage
停止,CPage
默认返回RootPage
。
popUntil
返回到指定的标记路由,若标记的路由为null
,则程序退出,慎用!!!
有时候我们需要根据业务需求判断:可能返回上一级路由,也可能返回上上级路由或是返回指定的路由等。这个时候就不能使用Replacemen
t和RemoveUntil
来替换、移除路由了。
例如:
Navigator.of(context).popUntil((route) => route.settings.name == "/");
或
Navigator.of(context).popUntil(ModalRoute.withName("/"));
再例如:
要实现上述功能,从
CPage
返回到APage
,并且不在MaterialApp
内的routes
属性进行显式声明路由。因为笔者觉得一个应用程序的界面太多了,如果每个界面都要显示声明路由,实在是不优雅。因为需要返回
APage
,还是需要标记路由,所有我们在之前跳转APage
的时候设置RouteSettings
,如下:
// 设置APage的RouteSettings
Navigator.of(context).push(MaterialPageRoute(
settings: RouteSettings(name:"/A"),
builder: (context) => APage(),
));
在CPage
需要返回的时候,调用就行:
Navigator.of(context).popUntil(ModalRoute.withName("/A"));
这样代码看起来很优雅,不会冗余。
另:
// 返回根路由
Navigator.of(context).popUntil((route) => route.isFirst);
canPop
用来判断是否可以导航到新页面,返回的bool
类型,一般是在设备带返回的物理按键时需要判断是否可以pop
。
maybePop
可以理解为canPop
的升级,maybePop
会自动判断。如果当前的路由可以pop
,则执行当前路由的pop
操作,否则将不执行。
removeRoute/removeRouteBelow
删除路由,同时执行Route.dispose
操作,无过渡动画,正在进行的手势也会被取消。
removeRoute
removeRoute.pngBPage
被移除了当前的路由栈。
如果在当前页面调用removeRoute
,则类似于调用pop
方法,区别就是无过渡动画,所以removeRoute
也可以用来返回上一页。
removeRouteBelow
移除指定路由底层的临近的一个路由,并且对应路由不存在的时候会报错。
同上。
综上:这个两个方法一般情况下很少用,而且必须要持有对应的要移除的路由。
一般用于立即关闭,如移除当前界面的弹出框等。
路由传值
常见的路由传值分为两个方面:
- 向下级路由传值
- 返回上级路由时传值
要注意的是,我们一般说静态路由不能传值,并不是说一定不能用于传值,而是因为静态路由一般需要在MaterialApp
内的routes
属性进行显式声明,在这里使用构造函数传值无实际意义。
如:
MaterialApp(
initialRoute: "/", // 默认加载的界面,这里为RootPage
routes: { // 显式声明路由
"/":(context) => RootPage(),
"/A":(context) => APage("title"), // 在这里传参无实际意义,一般需要传入的参数都是动态变化的
"/B":(context) => BPage(),
"/C":(context) => CPage(),
},
// home: RootPage(),
);
向下级路由传值
1、构造函数传值
首先构造一个可以带参数的构造函数:
class APage extends StatefulWidget {
String title;
APage(this.title);
@override
_APageState createState() => _APageState();
}
在路由跳转的时候传值:
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => APage("这是传入的参数"),
));
在APage拿到传入的值:
// 在 StatefulWidget 使用[widget.参数名]
Container(
child: Text(widget.title),
)
2、ModalRoute 传值
在Navigator.of(context).push
的跳转方式中,MaterialPageRoute
的构造参数中 可以看到有RouteSettings
的属性,RouteSettings
就是当前路由的基本信息
const RouteSettings({
this.name,
this.isInitialRoute = false,
this.arguments, // 存储路由相关的参数Object
});
路由跳转时设置传递参数:
Navigator.of(context).push(MaterialPageRoute(
settings: RouteSettings(name:"/A",arguments: {"argms":"这是传入A的参数"}),
builder: (context) => APage(),
));
或使用静态路由pushName:
Navigator.of(context).pushNamed("/A",arguments:{"argms":"这是传入A的参数"});
在APage
中取值:
Map argms = ModalRoute.of(context).settings.arguments;
print(argms["argms"]);
返回上级路由时传值
就是在调用APage
中调用pop
返回路由的时候传参
Navigator.of(context).pop("这是pop返回的参数值");
在上一级路由获取:
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => APage(),
)).then((value){ // 获取pop的传值
print(value);
});
或
String value = await Navigator.of(context).pushNamed('/xxx');