一,概述
移动应用通常通过成为‘屏幕’或者‘页面’的全屏元素显示其内容,在Flutter中,这些元素统称为路由,它们由导航器Navigator组件管理。导航器管理一组路由Route对象,并提供了管理堆栈的方法,例如Navigator.push和Navigator.pop。
如果类比这Ios记忆的话,你可以粗略地把一个路由对应到一个 UIViewController。Navigator的工作原理和 iOS 中 UINavigationController 非常相似,当你想跳转到新页面或者从新页面返回时,它可以 push() 和 pop() 路由。
在Flutter中,有两个主要的widget用于在页面之间导航:
- Route是一个应用程序抽象的屏幕或页面;
- Navigator 是一个管理路由的widget;
以上两种widget对应Flutter中实现页面导航的有两种选择:
- 静态路由:具体指定一个由路由名构成的 Map。(MaterialApp)
- 动态路由:直接跳转到一个路由。(WidgetApp)
二,页面跳转的两个选择
Flutter中,跳转页面有两种方式:静态路由(又可称为命名路由)方式和动态路由(又可称为组件路由)方式。在Flutter管理多个页面有两个核心概念和类:Route和Navigator。一个route是一个屏幕或者页面的抽象,Navigator是管理route的Widget。Navigator可以通过route入栈和出栈来实现页面之间的跳转。
- 静态路由(又称命名路由,它是将应用中需要访问的每个页面命名为不重复的字符串,我们便可以通过这个字符串来将该页面实例推进路由)
说明:'/ xxx' 表示 xxx页面,'/' 表示主页面。 这里的命名规范与 REST API 开发中的路由类似。 所以 '/' 通常表示的是我们的根页面。
- 配置路由
- 点击跳转
- 配置新页面
新页面,我在lib
下建立一个新的文件(页面)mainfourday.dart
,很简单: - 最终效果如下:
在原页面配置路由跳转,就是在MaterialApp里设置每个route对应的页面,注意:一个app只能有一个材料设计(MaterialApp),不然返回上一个页面会黑屏。
代码如下:
//入口页面
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
//静态路由方式 配置初始路由
initialRoute: '/',
routes: {
//默认走这个条件`/`
'/':(BuildContext context){
return HomeStateful();
},
//新页面路由
'/mainnewroute':(BuildContext context){
return new newRoute();
}
},
//主题色
theme: ThemeData(
//设置为红色
primarySwatch: Colors.red),
//配置了初始路由,下面就不需要了
//home: HomeStateful(),
);
}
}
因为配置了初始路由,所以home:HomeStateful
就不用配置了。
//如果新页面不在同一个类中,记得把它导入
import 'mainnewroute.dart';
class HomeStateful extends StatefulWidget{
@override
State<StatefulWidget> createState(){
return new HomeWidget();
}
}
class HomeWidget extends State<HomeStateful> {
@override
Widget build(BuildContext context) {
...
//Pointer
Widget TestContainer = Listener(
child:Container(
width: 300.0,
height: 300.0,
color:Colors.red,
child: RaisedButton(
child: Text('点击我'),
onPressed: (){
//页面跳转方法
Navigator.of(context).pushNamed('/mainnewroute');
}),
),
);
return new Scaffold(
appBar: new AppBar(
title: new Text('Flutter Demo'),
),
body:Center(
child: TestContainer,
),
);
}
}
RaisedButton
配置了点击方法,上面用了Navigator.of(context).pushNamed('/mainnewroute')
,执行到这句,路由会找routes
有没有配置/mainnewroute
,有的话,就会根据配置跳到新的页面。
import 'package:flutter/material.dart';
class newRoute extends StatelessWidget{
@override
Widget build(BuildContext context){
return HomeWidget();
//注意:不需要MaterialApp
// return MaterialApp(
// theme: ThemeData(
// //设置为hongse
// primarySwatch: Colors.red),
// home: HomeWidget(),
// );
}
}
class HomeWidget extends StatelessWidget{
@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text('new Route'),
),
body: Center(
child:RaisedButton(
child: Text('返回'),
onPressed: (){
//这是关闭页面
Navigator.pop(context);
}),
// child: Text('这是新的页面'),
),
);
}
}
- 动态路由(又称组件路由,要在堆栈上推送新的实例,我们可以调用导航器
Navigator.push
,传入当前 context 并且使用构建器函数创建 MaterialPageRoute 实例,该函数可以创建您想要在屏幕上显示的内容)
下面说一下跳转页面的第二种方式,动态路由方式:
child: RaisedButton(
child: Text('点击我'),
onPressed: (){
//Navigator.of(context).pushNamed('/mainnewroute');
//动态路由
Navigator.push(
context,
MaterialPageRoute(builder: (newPage){
return new newRoute();
}),
);
}),
代码效果和上面是一样的。
三,详解路由栈
前面,我们已经知道如何简单在路由栈中 push、pop 实例,然而,当遇到一些特殊的情况,这显然不能满足需求。Flutter 当然也有类似的可以解决各种业务需求的实现方式!
pushReplacementNamed 与 popAndPushNamed
- 假如我们定义了四个静态路由,分别为"/Screen1","/Screen2","/Screen3","/Screen4"
RaisedButton(
onPressed: () {
Navigator.pushReplacementNamed(context, "/screen4");
},
child: Text("pushReplacementNamed"),
),
RaisedButton(
onPressed: () {
Navigator.popAndPushNamed(context, "/screen4");
},
child: Text("popAndPushNamed"),
),
- 我们在 Screen3 页面使用
pushReplacementNamed
与popAndPushNamed
方法 push 了 Screen4。
此时路由栈情况如下:
Screen4 代替了 Screen3。pushReplacementNamed
与popAndPushNamed
的区别在于:
popAndPushNamed能够执行Screen2 弹出的动画与 Screen3 推进的动画而 pushReplacementNamed仅显示 Screen3 推进的动画。
- 案例:
pushReplacementNamed:当用户成功登录并且现在在
HomeScreen
上时,您不希望用户还能够返回到LoginScreen
。因此,登录应完全由首页替换。另一个例子是从SplashScreen
转到HomeScreen
。 它应该只显示一次,用户不能再从HomeScreen
返回它。 在这种情况下,由于我们要进入一个全新的屏幕,我们可能需要借助此方法。popAndPushNamed:假设您正在有一个
Shopping
应用程序,该应用程序在ProductsListScreen
中显示产品列表,用户可以在FiltersScreen
中应用过滤商品。 当用户单击“应用筛选”按钮时,应弹出FiltersScreen
并使用新的过滤器值推回到ProductsListScreen
。 这里popAndPushNamed
显然更为合适。
pushNamedAndRemoveUntil
用户已经登陆进入
HomeScreen
,然后经过一系列操作回到配合界面想要退出登录,你不能够直接 Push 进入LoginScreen
吧?你需要将之前路由中的实例全部删除,是的用户不会在回到先前的路由中。
- pushNamedAndRemoveUntil 可实现该功能:
Navigator.of(context).pushNamedAndRemoveUntil('/screen4', (Route<dynamic> route) => false);
这里的 (Route<dynamic> route) => false
能够确保删除先前所有实例。
- 现在又有一个需求:我们不希望删除先前所有实例,我们只要求删除指定个数的实例。
我们有一个需要付款交易的购物应用。在应用程序中,一旦用户完成了支付交易,就应该从堆栈中删除所有与交易或购物车相关的页面,并且用户应该被带到
PaymentConfirmationScreen
,单击后退按钮应该只将它们带回到ProductsListScreen
或HomeScreen
。
Navigator.of(context).pushNamedAndRemoveUntil('/screen4', ModalRoute.withName('/screen1'));
通过代码,我们推送 Screen4
并删除所有路由,直到 Screen1
:
popUntil
想象一下,我们在应用程序中要填写一系列信息,表单分布在多个页面中。假设需要填写三个页面的表单一步接着一步。 然而,在表单的第 3 部分,用户取消了填写表单。 用户单击取消并且应弹出所有之前与表单相关的页面,并且应该将用户带回 HomeScreen或者 DashboardScreen,这种情况下数据属于数据无效! 我们不会在这里推新任何新东西,只是回到以前的路由栈中。
Navigator.popUntil(context, ModalRoute.withName('/screen2'));
Popup routes(弹出路由)
- 路由不一定要遮挡整个屏幕。 PopupRoutes 使用 ModalRoute.barrierColor覆盖屏幕,ModalRoute.barrierColor只能部分不透明以允许当前屏幕显示。 弹出路由是“模态”的,因为它们阻止了对下面其他组件的输入。
- 有一些方法可以创建和显示这类弹出路由。 例如:showDialog,showMenu 和 showModalBottomSheet。 如上所述,这些函数返回其推送路由的 Future(异步数据,参考下面的数据部分)。 执行可以等待返回的值在弹出路由时执行操作。
- 还有一些组件可以创建弹出路由,如 PopupMenuButton 和 DropdownButton。 这些组件创建 PopupRoute 的内部子类,并使用 Navigator 的push 和 pop 方法来显示和关闭它们。
自定义路由
您可以创建自己的一个窗口z组件库路由类(如 PopupRoute,ModalRoute 或 PageRoute)的子类,以控制用于显示路径的动画过渡,路径的模态屏障的颜色和行为以及路径的其他各个特性。
PageRouteBuilder 类可以根据回调定义自定义路由。 下面是一个在路由出现或消失时旋转并淡化其子节点的示例。 此路由不会遮挡整个屏幕,因为它指定了opaque:false,就像弹出路由一样。
Navigator.push(context, PageRouteBuilder(
opaque: false,
pageBuilder: (BuildContext context, _, __) {
return Center(child: Text('My PageRoute'));
},
transitionsBuilder: (___, Animation<double> animation, ____, Widget child) {
return FadeTransition(
opacity: animation,
child: RotationTransition(
turns: Tween<double>(begin: 0.5, end: 1.0).animate(animation),
child: child,
),
);
}
));
- 路由两部分构成,“pageBuilder”和“transitionsBuilder”。
该页面成为传递给 buildTransitions 方法的子代的后代。 通常,页面只构建一次,因为它不依赖于其动画参数(在此示例中以_和__表示)。 过渡是建立在每个帧的持续时间。
嵌套路由
一个应用程序可以使用多个路由导航器。将一个导航器嵌套在另一个导航器下方可用于创建“内部旅程”,例如选项卡式导航,用户注册,商店结帐或代表整个应用程序子部分的其他独立个体。
iOS应用程序的标准做法是使用选项卡式导航,其中每个选项卡都维护自己的导航历史记录。因此,每个选项卡都有自己的导航器,创建了一种“并行导航”。
除了选项卡的并行导航之外,还可以启动完全覆盖选项卡的全屏页面。例如:入职流程或警报对话框。因此,必须存在位于选项卡导航上方的“根”导航器。因此,每个选项卡的 Navigators 实际上都是嵌套在一个根导航器下面的Navigators。
用于选项卡式导航的嵌套导航器位于 WidgetApp 和 CupertinoTabView中,因此在这种情况下您无需担心嵌套的导航器,但它是使用嵌套导航器的真实示例。
以下示例演示了如何使用嵌套的 Navigator 来呈现独立的用户注册过程。
尽管此示例使用两个 Navigators 来演示嵌套的 Navigators,但仅使用一个 Navigato r就可以获得类似的结果。
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
// ...some parameters omitted...
// MaterialApp contains our top-level Navigator
initialRoute: '/',
routes: {
'/': (BuildContext context) => HomePage(),
'/signup': (BuildContext context) => SignUpPage(),
},
);
}
}
class SignUpPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
// SignUpPage builds its own Navigator which ends up being a nested
// Navigator in our app.
return Navigator(
initialRoute: 'signup/personal_info',
onGenerateRoute: (RouteSettings settings) {
WidgetBuilder builder;
switch (settings.name) {
case 'signup/personal_info':
// Assume CollectPersonalInfoPage collects personal info and then
// navigates to 'signup/choose_credentials'.
builder = (BuildContext _) => CollectPersonalInfoPage();
break;
case 'signup/choose_credentials':
// Assume ChooseCredentialsPage collects new credentials and then
// invokes 'onSignupComplete()'.
builder = (BuildContext _) => ChooseCredentialsPage(
onSignupComplete: () {
// Referencing Navigator.of(context) from here refers to the
// top level Navigator because SignUpPage is above the
// nested Navigator that it created. Therefore, this pop()
// will pop the entire "sign up" journey and return to the
// "/" route, AKA HomePage.
Navigator.of(context).pop();
},
);
break;
default:
throw Exception('Invalid route: ${settings.name}');
}
return MaterialPageRoute(builder: builder, settings: settings);
},
);
}
}
Navigator.of 在给定 BuildContext 中最近的根 Navigator 上运行。 确保在预期的 Navigator 下面提供BuildContext,尤其是在创建嵌套 Navigators 的大型构建方法中。 Builder 组件可用于访问组件子树中所需位置的 BuildContext。
四,页面跳转发送数据
页面跳转时有时需要发送数据到第二个页面,比如从订单列表到商品详情页面时,通常需要发送商品ID参数。
- 动态路由传递参数
Navigator.push(
context,
MaterialPageRoute(builder: (newPage){
return new newRoute("这是一份数据到新页面");
}),
);
在新页面改为如下:
import 'package:flutter/material.dart';
class newRoute extends StatelessWidget{
//接收上一个页面传递的数据
String str;
//构造函数
newRoute(this.str);
@override
Widget build(BuildContext context){
return HomeWidget(str);
}
}
class HomeWidget extends StatelessWidget{
String newDate;
HomeWidget(this.newDate);
@override
Widget build(BuildContext context){
return Scaffold(
appBar: AppBar(
title: Text('new Route'),
),
body: Center(
child:RaisedButton(
//显示上一个页面所传递的数据
child: Text(newDate),
onPressed: (){
Navigator.pop(context);
}),
// child: Text('这是新的页面'),
),
);
}
}
- 静态路由传递参数
静态路由方式传递参数,也就是在newRoute()加上所要传递的参数就可以了
//新页面路由
'/mainnewroute':(context){
return new newRoute("sdsd");
}
四,页面跳转返回数据
- 数据传递
在上面的大多数示例中,我们推送新路由时没有发送数据,但在实际应用中这种情况应用很少。 要发送数据,我们将使用 Navigator 将新的 MaterialPageRoute 用我们的数据推送到堆栈上(这里是 userName)。
String userName = "John Doe";
Navigator.push(
context,
new MaterialPageRoute(
builder: (BuildContext context) =>
new Screen5(userName)));
要在 Screen5 中得到数据,我们只需在 Screen5 中添加一个参数化构造函数:
class Screen5 extends StatelessWidget {
final String userName;
Screen5(this.userName);
@override
Widget build(BuildContext context) {
print(userName)
...
}
}
这表示我们不仅可以使用 MaterialPageRoute 作为 push 方法,还可以使用 pushReplacement ,pushAndPopUntil 等。基本上从我们描述的上述方法中路由方法,第一个参数现在将采用 MaterialPageRoute 而不是 namedRoute 的 String。
- 数据返回
我们可能还想从新页面返回数据。 就像一个警报应用程序,并为警报设置一个新音调,您将显示一个带有音频音调选项列表的对话框。 显然,一旦弹出对话框,您将需要所选的项目数据。 它可以这样实现:
new RaisedButton(onPressed: ()async{
String value = await Navigator.push(context, new MaterialPageRoute<String>(
builder: (BuildContext context) {
return new Center(
child: new GestureDetector(
child: new Text('OK'),
onTap: () { Navigator.pop(context, "Audio1"); }
),
);
}
)
);
print(value);
},
child: new Text("Return"),)
在 Screen4 中尝试并检查控制台的打印值。
五,其他效果解释
- maybePop
如果我们在初始路由上并且有人错误地试图弹出这个唯一页面怎么办? 弹出堆栈中唯一的页面将关闭您的应用程序,因为它后面已经没有页面了。这显然是不好的体验。 这就是 maybePop() 起的作用。 点击 Screen1 上的 maybePop 按钮,没有任何效果。 在 Screen3 上尝试相同的操作,可以正常弹出。
static Future<bool> maybePop<T extends Object>(BuildContext context, [ T result ]) {
return Navigator.of(context).maybePop<T>(result);
}
@optionalTypeArgs
Future<bool> maybePop<T extends Object>([ T result ]) async {
final Route<T> route = _history.last;
assert(route._navigator == this);
final RoutePopDisposition disposition = await route.willPop();
if (disposition != RoutePopDisposition.bubble && mounted) {
if (disposition == RoutePopDisposition.pop){
pop(result);
return true;
}
}
return false;
}
- canPop(maybePop效果也可通过 canPop 实现:)
static bool canPop(BuildContext context) {
final NavigatorState navigator = Navigator.of(context, nullOk: true);
return navigator != null && navigator.canPop();
}
bool canPop() {
assert(_history.isNotEmpty);
return _history.length > 1 || _history[0].willHandlePopInternally;
}
如果占中实例大于 1 或 willHandlePopInternally 属性为 true 返回 true,否则返回 false。
我们可以通过判断 canPop 来确定是否能够弹出该页面。
- 如何去除默认返回按钮
AppBar({
Key key,
this.leading,
this.automaticallyImplyLeading = true,
this.title,
this.actions,
this.flexibleSpace,
this.bottom,
this.elevation = 4.0,
this.backgroundColor,
this.brightness,
this.iconTheme,
this.textTheme,
this.primary = true,
this.centerTitle,
this.titleSpacing = NavigationToolbar.kMiddleSpacing,
this.toolbarOpacity = 1.0,
this.bottomOpacity = 1.0,
}) : assert(automaticallyImplyLeading != null),
assert(elevation != null),
assert(primary != null),
assert(titleSpacing != null),
assert(toolbarOpacity != null),
assert(bottomOpacity != null),
preferredSize = Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)),
super(key: key);
将 automaticallyImplyLeading置为 false