原文:Flutter Navigation Tutorial
作者:Joe Howard
译者:kmyhy
比只有一屏的 app 更好的是什么?当然是有两屏的 app 了:]
导航是移动 app UX 的重要组成部分。由于手机屏幕资源有限,用户需要不停地在各个屏幕之间进行导航,例如,从一个表格导航到详情屏幕,从购物车导航到结算屏,从菜单导航到表单,等等。一个良好的导航能帮助用户不迷失方向并在宽广的 app 中进退自如。
iOS 的导航通常是哟 UINavigationController,这是一个栈式的屏幕切换方式。在 Android 中,则主要用 activity 栈为用户导航。在这些栈中,两个屏幕之间的动画是不一样的,从而导致 app 风格不一。
和原生 SDK 一样,跨平台开发框架也为 app 提供了屏幕切换方式。大部分情况下,你想让每个平台上的导航方式和用户预期的保持一致。
Flutter 是一个 Google 的跨平台开发 SDK,允许你基于同一套代码快速创建 iOS 和 Android app。如果你没有接触过 Flutter,请阅读我们的 Flutter 开始教程,以了解基本的 Flutter 使用。
在本教程中,你将了解 Flutter 如何在一个跨平台 app 中实现屏幕间的导航,包括:
你可以在本文头部或底部下载开始项目。
本教程将使用安装了 Flutter 扩展的 VSCode。你也可以用 IntelliJ IDEA 或 Android Studio 或任意文本编辑工具并在命令行中使用 Flutter。
在 VSCode 中选择 File > Open 并找到开始项目解压后的根文件夹,打开开始项目:
VSCode 会提示下载项目要用到的包,请根据提示进行:
项目打开后,按 F5 ,build & run。如果 VSCode 提示你选择 app 执行环境,请选择 Dart & Flutter:
这是在 iOS 模拟器中运行项目:
这是在 Android 模拟器中运行项目:
“slow mode” 标志显示,表示你正在以 debug 模式运行 app。
开始 app 显示了一个 GitHub 组织的成员列表。在本教程中,我们将从这个第一屏导航到每个成员单独的屏幕。
首先要创建针对每个成员的屏幕画面。每个 Flutter UI 的元素都是 UI widget,因此我们需要创建 member widget。
首先,右键点击项目中的 lib 文件夹,选择 New File,创建一个新文件,名为 memberwidget.dart:
添加导入语句,添加一个 StatefulWidget 子类,名为 MemberWidget:
import 'package:flutter/material.dart';
import 'member.dart';
class MemberWidget extends StatefulWidget {
final Member member;
MemberWidget(this.member) {
if (member == null) {
throw new ArgumentError("member of MemberWidget cannot be null. "
"Received: '$member'");
}
}
@override
createState() => new MemberState(member);
}
MemberWidget 用 MemberState 类来保存它的状态,并传递一个 Member 对象给 MemberState。在 widget 的构造函数中,确保 member 参数不为空。
在同一文件中 MemberWidget 的上面添加一个 MemberState 类:
class MemberState extends State<MemberWidget> {
final Member member;
MemberState(this.member);
}
这里声明了一个 Member 属性和一个构造函数。
每个 widget 都必须重写 build() 方法,因此在 MemberState 中重写该方法:
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text(member.login),
),
body: new Padding(
padding: new EdgeInsets.all(16.0),
child: new Image.network(member.avatarUrl)
)
);
}
这里创建了一个 Scaffold,即材料设计的容器,它包含了一个 AppBar 和一个child 为成员头像 Image 的 Padding widget。
成员的屏幕已经写好,接下来可以进行导航了:]
在 Flutter 中导航是基于路由概念的。
路由就好比 REST API 中的路由概念,每个路由都是相对于根的。app 中的 main() 方法所创建的 widget 就是根。
一种使用路由的方法就是 PageRoute 类。因为当前 app 是一个 Flutter Material App,你需要用的是 MaterialPageRoute 子类。
在 GHFlutterState 头部加入 import 语句,以便能够调用 member widget:
import 'memberwidget.dart';
然后在 ghflutterwidget.dart 中为 GHFlutterState 添加私有方法:
_pushMember(Member member) {
Navigator.of(context).push(
new MaterialPageRoute(
builder: (context) => new MemberWidget(member)
)
);
}
这里用 Navigator 来 push 一个 MaterialPageRoute 到导航栈中,而这个 MaterialPageRoute 用你的 MemberWidget 进行构造。
现在的用户点击表格行时需要调用 _pushMemeber()。你需要修改 GHFlutterState 中的 _buildRow() 方法,在 ListTile 中添加一个 onTap 属性:
Widget _buildRow(int i) {
return new Padding(
padding: const EdgeInsets.all(16.0),
child: new ListTile(
title: new Text("${_members[i].login}", style: _biggerFont),
leading: new CircleAvatar(
backgroundColor: Colors.green,
backgroundImage: new NetworkImage(_members[i].avatarUrl)
),
// Add onTap here:
onTap: () { _pushMember(_members[i]); },
)
);
}
当行被点击,_pushMember() 方法被调用,同时传递所选中的 member。
按 F5 build & run。点击某一行,你会看到成员详情屏显示:
这是在 iOS 中运行的效果:
注意,Android 中的返回按钮是 Android 风格的,而 iOS 中的返回按钮是 iOS 风格的,屏幕转换动画和对应平台保持一致。
点击返回按钮,回到列表页,如果你想用在 app 中用你自己的按钮来触发返回该怎么做?
因为在 Flutter app 中的导航使用栈,同时你已经 push 了一个新的屏幕 widget 到栈中,那么为了返回上一屏,你必须对栈进行 pop 操作。
修改 MemberState 的 build() 方法,添加一个 IconButton,将 Image 替换成一个 Column widget:
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text(member.login),
),
body: new Padding(
padding: new EdgeInsets.all(16.0),
// Add Column here:
child: new Column(
children: [
new Image.network(member.avatarUrl),
new IconButton(
icon: new Icon(Icons.arrow_back, color: Colors.green, size: 48.0),
onPressed: () { Navigator.pop(context); }
),
]),
)
);
}
为了将 Image 和 IconButton 垂直布局,你添加了一个 Column。对于 IconButton,你将 onPressed 属性设置为调用 Navigator 来对栈进行 pop。
按 F5 build & run,你可以点击新加的返回箭头来回到成员列表:
路由也可以有返回值,就像 Android 使用 onActivityResult() 来读取结果一样。
来看个简单例子,在 MemberState 中添加下列私有的异步方法:
_showOKScreen(BuildContext context) async {
// 1, 2
bool value = await Navigator.of(context).push(new MaterialPageRoute<bool>(
builder: (BuildContext context) {
return new Padding(
padding: const EdgeInsets.all(32.0),
// 3
child: new Column(
children: [
new GestureDetector(
child: new Text('OK'),
// 4, 5
onTap: () { Navigator.of(context).pop(true); }
),
new GestureDetector(
child: new Text('NOT OK'),
// 4, 5
onTap: () { Navigator.of(context).pop(false); }
)
])
);
}
));
// 6
var alert = new AlertDialog(
content: new Text((value != null && value) ? "OK was pressed" : "NOT OK or BACK was pressed"),
actions: [
new FlatButton(
child: new Text('OK'),
// 7
onPressed: () { Navigator.of(context).pop(); }
)
],
);
// 8
showDialog(context: context, child: alert);
}
这个方法主要做了一下几个事情:
上面需要注意的是 MaterialPageRoute 中的类型参数 bool,你可以将它替换成你想从路由中返回的任意类型,同时你需要将返回值在调用 pop 时传入,例如 Navigator.of(context).pop(true)。
修改 MemberState 的 build() 方法,添加一个 RaisedButton 按钮来调用 _showOKScreen():
@override
Widget build(BuildContext context) {
return new Scaffold (
appBar: new AppBar(
title: new Text(member.login),
),
body: new Padding(
padding: new EdgeInsets.all(16.0),
child: new Column(
children: [
new Image.network(member.avatarUrl),
new IconButton(
icon: new Icon(Icons.arrow_back, color: Colors.green, size: 48.0),
onPressed: () { Navigator.pop(context); }
),
// Add RaisedButton here:
new RaisedButton(
child: new Text('PRESS ME'),
onPressed: () { _showOKScreen(context); }
)
]),
)
);
}
这个 RaiseButton 用于显示新屏幕。
按 F5 开始 build & run。点击 PRESS ME 按钮,然后点 OK 或者 NOT OK 或者返回按钮。你会从新屏幕中看到用户点击的结果:
为了给你的 app 的导航添加一点独特的味道,你可以自定义转换动画。你可以扩展 PageRoute 类,也可以用 PageRouteBuilder 之类的类通过回调方式自定义路由。
修改 GHFlutterState 的 _pushMember 方法,push 一个 PageRouteBuilder 入栈:
_pushMember(Member member) {
// 1
Navigator.of(context).push(new PageRouteBuilder(
opaque: true,
// 2
transitionDuration: const Duration(milliseconds: 1000),
// 3
pageBuilder: (BuildContext context, _, __) {
return new MemberWidget(member);
},
// 4
transitionsBuilder: (_, Animation animation, __, Widget child) {
return new FadeTransition(
opacity: animation,
child: new RotationTransition(
turns: new Tween(begin: 0.0, end: 1.0).animate(animation),
child: child,
),
);
}
));
}
这里,你:
按 F5 进行 build & run,看一下新动画的样子:
噢,这真让我有点头晕!:]
你可以从本教程头部或底部下载完整项目。
通过访问下列网址,你可以学习更多 Flutter 导航的知识:
在阅读文档时,请尤其注意阅读如何创建命名路由,这样你就可以调用 Navigator 的 pushNamed() 来调用路由了。
请继续关注更多的 Flutter 教程和屏播!
请在论坛或评论中提问,分享你的心得。希望你学得愉快!
Download Materials