Flutter第一部分(UI)第七篇:路由导航实践
前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件、数据库、网络)再到Flutter进阶(平台特定代码编写、测试、插件开发等),欢迎感兴趣的读者持续关注(可以扫描左边栏二维码或者微信搜索”IT工匠“关注微信公众号哦,会同步推送)。
路由机制其实没有太多的理论知识,所以本文主要针对实际生产中的几个场景介绍具体的实现方法,相信通过本文的学习,大家足够应对日常的路由跳转问题。
本文的主要内容如下:
- 如何导航到一个新的页面并返回
- 如何使用命名路由进行导航
- 向新的页面发送数据
- 从当前页面返回数据给前一个页面
- 导航时的动画处理
导航到新的页面并返回
大多数app
都包含不至页面,因为多个页面可以显示多种不同类型的信息。比如, app
可能具有一个展示产品列表的页面,然后,,当用户可以点击产品列表中的某一件商品时app
又会跳转到另一个新的页面去展示被点击商品的详细信息。
术语解释:在
Flutter
中,屏幕(screen
)和页面(page
)被称为路由(route
)。
在Android
中,一个路由相当于一个Activity
,在Ios
中,一个路由相当于一个ViewController
,而在Flutter
中,一个路由指的是一个Widget
。
那么我们应该如何在Flutter中进行路由的跳转的?答案是使用Navigator
类。
接下来我将通过一个简单的实例来向大家展示在Flutter
中应该如何正确地使用进行Navigator
类路由的跳转,整个实例共分为3步:
- 创建2个路由
- 使用
Navigator.push()
跳转到第二个路由 - 使用
Navigator.pop()
返回到第一个路由
创建2个路由
首先, 创建两个即将要用到的路由。由于这是只一个简单的示例程序, 所以每个路由中只包含一个按钮,要实现的功能是点击第一个路由上的按钮会导航到第二个路由,点击第二个路由上的按钮会返回到第一条路由。
创建路由的逻辑很简单,给出主要代码:
class FirstRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Route'),
),
body: Center(
child: RaisedButton(
child: Text('Open route'),
onPressed: () {
// 点击事件触发时跳转到第二个路由
},
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: Center(
child: RaisedButton(
onPressed: () {
// 点击事件触发时返回到第一个路由
},
child: Text('Go back!'),
),
),
);
}
}
使用Navigator.push()跳转到第二个路由
我们使用Navigator.push()
来跳转到第二个路由,这个push()
方法可以接收一个Route
类型的参数,作用是将这个Route
类加入到由Navigator
管理的一个路由栈中,那么这个Route
类型的参数应该传什么?这里一般我们有两种选择,一种是我们自己手动创建一个Route
类,另一种是使用MaterialPageRoute
类,使用MaterialPageRoute
类的好处是它提供了平台特定的跳转动画。
注意:有的时候我们可能会出现跳转到第二个路由后不希望用户点击返回按钮后返回到前一个路由,常见的场景是登录页面点击登录进入主页面后不希望在主页面点击返回按钮之后返回到登录页面,这种情况下使用
Navigator.pushAndRemoveUntil()
替代Navigator.push()
即可。
具体的代码上我们可以这样实现:
// FirstRoute类中对应的代码
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
}
效果图如下图所示:
使用Navigator.pop()返回到第一个路由
上一小节已经实现了从第一个路由到第二个路由的跳转,那么跳转到第二个路由之后如果我们想要返回第一个路由应该怎么做?答案是使用Navigator.pop(),这里的pop()方法会移除路由栈中栈顶的路由(也就是当前路由),自然也就返回到了前一个路由。
具体到代码上是这样实现的:
//在SecondRoute的点击事件中
onPressed: () {
Navigator.pop(context);
}
运行效果是这样的:
完整的代码及效果
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Assets demo',
debugShowCheckedModeBanner: false,
home: FirstRoute());
}
}
class FirstRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Route'),
),
body: Center(
child: RaisedButton(
child: Text('Open route'),
onPressed: () {
// Navigate to second route when tapped.
Navigator.push(
context,
MaterialPageRoute(builder: (context) => SecondRoute()),
);
},
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Route"),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
),
);
}
}
完整的效果:
使用命名路由进行导航
在上一节我们介绍了如何导航到新的路由以及如何从路由中返回,这一节我们来介绍一下Flutter
中的**"命名路由”**。为什么要有命名路由?试想一下,如果我们需要从app
的多个位置导航到同一个路由,这种情况下如何使用上一节介绍的方法进行导航,就会造成代码的重复,所以指明命名路由来简化这一问题。
我们可以通过使用Navigator.pushNamed()
方法实现命名路由,接下来我会通过一个实例来介绍如何使用命名路由代替上一节介绍的路由跳转的方法。
主要分为4步:
- 创建两个页面
- 定义路由
- 使用
Navigator.pushNamed()
导航到第二个页面 - 使用
Navigator.pop()
导航到第一个路由
创建两个页面
首先我们需要创建两个页面,第一个页面上有一个导航到第二个屏幕的按钮,第二个页面上有一个返回到第一个屏幕的按钮,具体的实现代码如下:
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Screen'),
),
body: Center(
child: RaisedButton(
child: Text('Launch screen'),
onPressed: () {
// Navigate to second screen when tapped!
},
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: Center(
child: RaisedButton(
onPressed: () {
// Navigate back to first screen when tapped!
},
child: Text('Go back!'),
),
),
);
}
}
定义路由
接下来我们需要通过为MaterialApp
的构造函数提供initialRoute
和routes
参数来定义我们的路由。initialRoute
定义了app
启动时初始应该启动哪个路由,routes
参数定义了可用的命名路由和应该在导航到这些路由时构建的Widget
,实现代码如下:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Assets demo',
debugShowCheckedModeBanner: false,
initialRoute: '/',
routes: {
// 当导航到'/'的时候跳转到FirstScreen
'/': (context) => FirstScreen(),
// 当导航到"/second" 的时候跳转到SecondScreen
'/second': (context) => SecondScreen(),
});
}
}
注意:当使用initialRoute
参数的时候就不能设置home
参数了。
导航到第二个页面
定义好路由之后我们就可以开始导航了,在这里例子中我们使用Navigator.pushNamed()
函数来实现,实现代码如下:
// FirstScreen
onPressed: () {
// 使用命名路由跳转到第二个页面
Navigator.pushNamed(context, '/second');
}
效果图如下:
返回到第一个页面
返回第一个页面使用的是Navigator.pop()
函数:
// SecondScreen
onPressed: () {
Navigator.pop(context);
}
效果图如下:
完整的代码及效果
完整代码如下:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Assets demo',
debugShowCheckedModeBanner: false,
initialRoute: '/',
routes: {
// 当导航到'/'的时候跳转到FirstScreen
'/': (context) => FirstScreen(),
// 当导航到"/second" 的时候跳转到SecondScreen
'/second': (context) => SecondScreen(),
});
}
}
class FirstScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('First Screen'),
),
body: Center(
child: RaisedButton(
child: Text('Launch screen'),
onPressed: () {
Navigator.pushNamed(context, '/second');
},
),
),
);
}
}
class SecondScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Second Screen"),
),
body: Center(
child: RaisedButton(
onPressed: () {
Navigator.pop(context);
},
child: Text('Go back!'),
),
),
);
}
}
完整效果图如下:
向新的页面发送数据
通常, 我们在导航到新的页面的同时还希望在向新页面发送一些数据,例如, 当点击列表中的某一项后我们想要将被点击项的信息传递给展示详细信息的页面。
在本例中, 我们将创建一个 Todos
列表。当 “待办事项” 被点击时, 我们将导航到一个新页面 ,该页面显示有关待办事项的详细信息,具体的实现分为4步:
- 定义一个
Todo
类 - 展示一个
Todo
类的列表 - 创建一个详细信息页面,用来展示
Todo
对象的详情 - 导航并传递数据到信息信息页
定义Todo类
首先,我们需要一个简单的代办事项的抽象类,我们称之为Todo
类,该类有两个成员变量:标题、描述:
class Todo {
final String title;
final String description;
Todo(this.title, this.description);
}
创建一个Todo类的列表
接下来, 我们要显示Todo
实例的列表,在本例中, 我们将生成20个待办事项, 并使用 ListView
显示它们。
生成Todo类的列表数据
final todos = List<Todo>.generate(
20,
(i) => Todo(
'Todo $i',
'A description of what needs to be done for Todo $i',
),
);
使用ListView展示Todo类的列表
ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
);
},
);
创建详细信息页面
现在, 我们将创建第二个页面,屏幕的标题将展示待办事项(Todo
对象)的标题(title
), 屏幕的正文将显示描述(description
),实现代码如下:
class DetailScreen extends StatelessWidget {
// 声明Todo对象的占位符
final Todo todo;
// 构造函数中需要传入Todo对象
DetailScreen({Key key, @required this.todo}) : super(key: key);
@override
Widget build(BuildContext context) {
// 基于Todo对象的内容构建UI
return Scaffold(
appBar: AppBar(
title: Text(todo.title),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Text(todo.description),
),
);
}
}
导航并传递数据到信息信息页
定义好详细信息页, 我们就可以执行导航了,在我们的示例中, 当用户点击我们列表中的某个 Todo
项时, 我们希望导航到详细信息页,在导航的同时将被点击的Todo对象传递给详细信息页。为此, 我们将为 ListTile Widget
编写一个 onTap
回调,在 onTap
回调中, 我们使用Navigator. push()
方法实现路由导航:
ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
// 当用户点击当前ListTile, 即导航到DetailScreen,导航的同时将当前Todo对象传递过去
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(todo: todos[index]),
),
);
},
);
完整的代码及效果
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Assets demo',
debugShowCheckedModeBanner: false,
home: FirstRoute());
}
}
class FirstRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Todo List'),
),
body: ListView.builder(
itemCount: todos.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(todos[index].title),
// 当用户点击当前ListTile, 即导航到DetailScreen,导航的同时将当前Todo对象传递过去
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DetailScreen(todo: todos[index]),
),
);
},
);
},
),
);
}
}
class DetailScreen extends StatelessWidget {
// 声明Todo对象的占位符
final Todo todo;
// 构造函数中需要传入Todo对象
DetailScreen({Key key, @required this.todo}) : super(key: key);
@override
Widget build(BuildContext context) {
// 基于Todo对象的内容构建UI
return Scaffold(
appBar: AppBar(
title: Text(todo.title),
),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Text(todo.description),
),
);
}
}
效果图如下:
从当前页面返回数据给前一个页面
在某些情况下, 我们可能希望从新页面返回数据给前一个页面。例如, 假设我们导航到了一个新页面, 新页面中有两个不同的按钮,当用户点击某个按钮后, 我们希望通知第一个页面用户点击的是哪一个, 以便前一个页面可以根据该信息执行不同的逻辑。
实现的方法还是使用Navigator.pop()
,这里还是以一个实例进行说明,实例的实现分为5步:
- 定义初始页面
- 为添加一个导航到选择页面的按钮
- 展示具有两个按钮的选择页面
- 当选择页面中的某一个按钮被点击后,关闭选择页面
- 在初始页面显示选择页面点击的是哪个按钮
定义初始页面
初始页面主要是展示一个按钮,当用户点击了这个按钮后导航到第二个页面(选择页面):
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Returning Data Demo'),
),
// 这里的SelectionButton将在下一小节实现
body: Center(child: SelectionButton()),
);
}
}
添加导航到选择页面的按钮
现在我们应该创建上一小节用到的SelectionButton
类了,该方法的作用是创建并返回一个按钮,这个按钮需要实现2个功能:
- 被点击后导航到选择页面
- 接收选择页面的返回结果
实现代码如下:
class SelectionButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
_navigateAndDisplaySelection(context);
},
child: Text('Pick an option, any option!'),
);
}
// 导航到选择页面并等待选择页面的返回结果
_navigateAndDisplaySelection(BuildContext context) async {
// Navigator.push()返回的Future类型的变量将在Selection中调用Navigator.pop()后完成
final result = await Navigator.push(
context,
// We'll create the SelectionScreen in the next step!
MaterialPageRoute(builder: (context) => SelectionScreen()),
);
}
}
展示具有2个按钮的选择页面
现在, 我们需要建立一个选择页面,它将包含两个按钮,当用户点击按钮时, 它应该关闭选择页面, 并让初始页面知道哪个按钮被点击了。
我们先来定义 UI
, 在下一步将实现数据的返回:
class SelectionScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pick an option'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: () {
},
child: Text('黑'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: () {
},
child: Text('白'),
),
)
],
),
),
);
}
}
当按钮被点击后关闭选择页面
现在, 我们要更新两个按钮的 Onpress
回调,为了将数据返回到第一个页面, 我们需要使用Navigator.pop ()
方法。
了这个Navigator.pop ()
方法可以接收一个可选的称为result
的参数,如果我们在调用它的时候提供了这个result
参数, 它将会把result
以Future
的形式返回,具体的我们来看实现:
“黑”
RaisedButton(
onPressed: () {
Navigator.pop(context, '黑');
},
child: Text('黑'),
);
“白”
RaisedButton(
onPressed: () {
Navigator.pop(context, '白');
},
child: Text('白'),
);
在初始页面将选择页面的返回值显示出来
// 导航到选择页面并等待选择页面的返回结果
_navigateAndDisplaySelection(BuildContext context) async {
// Navigator.push()返回的Future类型的变量将在Selection中调用Navigator.pop()后完成
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SelectionScreen()),
);
// 当选择页面的结果返回后,隐藏之前的snackbar,将结果显示在新的snackbar上
Scaffold.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("$result")));
}
完整的代码及效果
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Assets demo',
debugShowCheckedModeBanner: false,
home: HomeScreen());
}
}
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Returning Data Demo'),
),
// We'll create the SelectionButton Widget in the next step
body: Center(child: SelectionButton()),
);
}
}
class SelectionButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RaisedButton(
onPressed: () {
_navigateAndDisplaySelection(context);
},
child: Text('Pick an option, any option!'),
);
}
// 导航到选择页面并等待选择页面的返回结果
_navigateAndDisplaySelection(BuildContext context) async {
// Navigator.push()返回的Future类型的变量将在Selection中调用Navigator.pop()后完成
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (context) => SelectionScreen()),
);
// 当选择页面的结果返回后,隐藏之前的snackbar,将结果显示在新的snackbar上
Scaffold.of(context)
..removeCurrentSnackBar()
..showSnackBar(SnackBar(content: Text("$result")));
}
}
class SelectionScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Pick an option'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: () {
Navigator.pop(context, '黑');
},
child: Text('黑'),
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: RaisedButton(
onPressed: () {
Navigator.pop(context, '白');
},
child: Text('白'),
),
)
],
),
),
);
}
}
完成后的运行效果如下:
导航时的动画处理
作为一款优质的app
,我们不能仅限于实现页面之间的导航,更重要的要考虑如何使用一个优美的动画实现两个页面之间的优雅过渡(切换),本节将介绍如何为页面之间的导航添加动画,主要使用的是Flutter
为我们提供的Hero Widget
。
本节还是继续以实例的形式进行知识点的介绍,我们要实现的效果是, 在两个页面上显示相同的图像,当用户点击图像时, 对图像进行从第一个页面到第二个页面的动画处理。我们将通过以下3步实现一个示例:
- 创建两个显示同一张图片的页面
- 为第一个页面添加
Hero Widget
- 为第二个页面添加
Hero Widget
创建两个显示同一张图片的页面
我们首先来创建显示图片的页面:
class MainScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Main Screen'),
),
body: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return DetailScreen();
}));
},
child: Image.asset('images/pic.jpeg')),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Center(child: Image.asset('images/pic.jpeg')),
),
);
}
}
为第一个页面添加Hero Widget
为了使用动画对两个屏幕进行过渡, 我们需要将Image Widget
包裹在Hero Widget
中,Hero Widget
在使用的时候需要两个参数:
- tag:Hero的标识参数,两个页面的
Hero Widget
的这个参数必须一样 - child:需要被动画过渡的
Widget
具体的实现代码如下:
class MainScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Main Screen'),
),
body: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return DetailScreen();
}));
},
child: Hero(tag: 'bestGirl', child: Image.asset('images/pic.jpeg'))),
);
}
}
为第二个页面添加Hero Widget
为了实现两个页面的过渡,我们也需要使用Hero Widget
包裹第二个页面的Image Widget
,注意第二个页面Hero
中的tag
参数必须和第一个页面中的一样:
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Hero(tag: 'bestGirl', child: Image.asset('images/pic.jpeg'))),
);
}
}
完整的代码及效果
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Assets demo',
debugShowCheckedModeBanner: false,
home: MainScreen());
}
}
class MainScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Main Screen'),
),
body: GestureDetector(
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) {
return DetailScreen();
}));
},
child: Hero(tag: 'bestGirl', child: Image.asset('images/pic.jpeg'))),
);
}
}
class DetailScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () {
Navigator.pop(context);
},
child: Hero(tag: 'bestGirl', child: Image.asset('images/pic.jpeg'))),
);
}
}
完整效果如下:
嘿嘿,原谅我不厚道地给图中的小仙女打了码,我的女朋友怎么可以随便给你们看哈哈~
好了,至此我们已经介绍完了Flutter
中路由和导航的常见用法,希望大家可以通过通过本文对路由和导航有更深刻的认识,下一期文章我们将会介绍Flutter
中的动画,欢迎大家持续关注~