如果说 UI 框架的视图元素的基本单位是组件,那应用程序的基本单位就是页面了。对于拥有多个页面的应用程序而言,如何从一个页面平滑地过渡到另一个页面,我们需要有一个统一的机制来管理页面之间的跳转,通常被称为路由管理或导航管理。
路由管理
在 Flutter 中,页面之间的跳转是通过 Route 和 Navigator 来管理的:
Route 是页面的抽象,主要负责创建对应的界面,接收参数,响应 Navigator 打开和关闭;
而 Navigator 则会维护一个路由栈管理 Route,Route 打开即入栈,Route 关闭即出栈,还可以直接替换栈内的某一个 Route。
而根据是否需要提前注册页面标识符,Flutter 中的路由管理可以分为两种方式:
基本路由。无需提前注册,在页面切换时需要自己构造页面实例。
命名路由。需要提前注册页面标识符,在页面切换时通过标识符直接打开新的路由。
基本路由
在 Flutter 中,基本路由的使用方法和 Android/iOS 打开新页面的方式非常相似。要导航到一个新的页面,我们需要创建一个 MaterialPageRoute 的实例,调用 Navigator.push 方法将新页面压到堆栈的顶部。
其中,MaterialPageRoute 是一种路由模板,定义了路由创建及切换过渡动画的相关配置,可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画。
而如果我们想返回上一个页面,则需要调用 Navigator.pop 方法从堆栈中删除这个页面。
下面的代码演示了基本路由的使用方法:在第一个页面的按钮事件中打开第二个页面,并在第二个页面的按钮事件中回退到第一个页面:
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
//打开页面
onPressed: ()=> Navigator.push(context, MaterialPageRoute(builder: (context) => SecondScreen()));
);
}
}
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
// 回退页面
onPressed: ()=> Navigator.pop(context)
);
}
}
命名路由
基本路由使用方式相对简单灵活,适用于应用中页面不多的场景。而在应用中页面比较多的情况下,再使用基本路由方式,那么每次跳转到一个新的页面,我们都要手动创建 MaterialPageRoute 实例,初始化页面,然后调用 push 方法打开它,还是比较麻烦的。
要想通过名字来指定页面切换,我们必须先给应用程序 MaterialApp 提供一个页面名称映射规则,即路由表 routes,这样 Flutter 才知道名字与页面 Widget 的对应关系。
路由表实际上是一个 Map<String,WidgetBuilder>,其中 key 值对应页面名字,而 value 值则是一个 WidgetBuilder 回调函数,我们需要在这个函数中创建对应的页面。而一旦在路由表中定义好了页面名字,我们就可以使用 Navigator.pushNamed 来打开页面了。
下面的代码演示了命名路由的使用方法:在 MaterialApp 完成了页面的名字 second_page 及页面的初始化方法注册绑定,后续我们就可以在代码中以 second_page 这个名字打开页面了:
MaterialApp(
...
//注册路由
routes:{
"second_page":(context)=>SecondPage(),
},
);
//使用名字打开页面
Navigator.pushNamed(context,"second_page");
不过由于路由的注册和使用都采用字符串来标识,这就会带来一个隐患:如果我们打开了一个不存在的路由会怎么办?
在注册路由表时,Flutter 提供了 UnknownRoute 属性,我们可以对未知的路由标识符进行统一的页面跳转处理。
下面的代码演示了如何注册错误路由处理。和基本路由的使用方法类似,我们只需要返回一个固定的页面即可。
MaterialApp(
...
//注册路由
routes:{
"second_page":(context)=>SecondPage(),
},
//错误路由处理,统一返回UnknownPage
onUnknownRoute: (RouteSettings setting) => MaterialPageRoute(builder: (context) => UnknownPage()),
);
//使用错误名字打开页面
Navigator.pushNamed(context,"unknown_page");
页面参数
与基本路由能够精确地控制目标页面初始化方式不同,命名路由只能通过字符串名字来初始化固定目标页面。为了解决不同场景下目标页面的初始化需求,Flutter 提供了路由参数的机制,可以在打开路由时传递相关参数,在目标页面通过 RouteSettings 来获取页面参数。
下面的代码演示了如何传递并获取参数:使用页面名称 second_page 打开页面时,传递了一个字符串参数,随后在 SecondPage 中,我们取出了这个参数,并将它展示在了文本中。
//打开页面时传递字符串参数
Navigator.of(context).pushNamed("second_page", arguments: "Hey");
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
//取出路由参数
String msg = ModalRoute.of(context).settings.arguments as String;
return Text(msg);
}
}
除了页面打开时需要传递参数,对于特定的页面,在其关闭时,也需要传递参数告知页面处理结果。
与 Android 提供的 startActivityForResult 方法可以监听目标页面的处理结果类似,Flutter 也提供了返回参数的机制。在 push 目标页面时,可以设置目标页面关闭时监听函数,以获取返回参数;而目标页面可以在关闭路由时传递相关参数。
下面的代码演示了如何获取参数:在 SecondPage 页面关闭时,传递了一个字符串参数,随后在上一页监听函数中,我们取出了这个参数,并将它展示了出来。
class SecondPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: <Widget>[
Text('Message from first screen: $msg'),
RaisedButton(
child: Text('back'),
//页面关闭时传递参数
onPressed: ()=> Navigator.pop(context,"Hi")
)
]
));
}
}
class _FirstPageState extends State<FirstPage> {
String _msg='';
@override
Widget build(BuildContext context) {
return new Scaffold(
body: Column(children: <Widget>[
RaisedButton(
child: Text('命名路由(参数&回调)'),
//打开页面,并监听页面关闭时传递的参数
onPressed: ()=> Navigator.pushNamed(context, "second_page",arguments: "Hey").then((msg)=>setState(()=>_msg=msg)),
),
Text('Message from Second screen: $_msg'),
],),
);
}
}
对于基本路由,如何传递页面参数?
- 使用构造函数传参
- MaterialPageRoute 加入参数 setting: RouteSettings 。第二个页面获取逻辑和命名路由就一样了。
Navigator.push(
context,
MaterialPageRoute(
// 传递title为SecondPage,跳转到第二个界面就会把标题设置为SecondPage
builder: (context) => SecondPage(title: "SecondPage"),
));
},
多参数传参
传参:
RaisedButton(
child: Text('命名路由(参数&回调)'),
onPressed: () =>
Navigator.pushNamed(context, "third_page", arguments: [1, 2])
.then((msg) {
setState(() {
_msg = msg;
});
})
计算并返回:
class ThirdPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
var args = ModalRoute.of(context).settings.arguments as List<int>;
int sum = args[0] + args[1];
print(sum);
return Scaffold(
appBar: AppBar(
title: Text('Third Screen'),
),
body: Column(
children: <Widget>[
Text('Message from first screen: $args'),
RaisedButton(
child: Text('back'),
onPressed: ()=> Navigator.pop(context, "${sum}"),
)
],
),
);
}
}