Flutter进阶—路由和导航
大部分应用程序都有多个屏幕或页面,并希望用户能从当前屏幕平滑过渡到另一个屏幕,Flutter的路由和导航功能可以帮助我们管理应用程序中的用户界面之间的命名和过渡。
管理多个用户界面有两个核心概念和类:路由(Route)和导航器(Navigator),路由(Route)是应用程序的“屏幕”或“页面”的抽象,导航器(Navigator)是管理路由的控件。导航器(Navigator)可以推送(push)和弹出(pop)路由来帮助用户从当前屏幕移动到另一个屏幕。
使用导航器
移动应用通常通过称为“屏幕”或“页面”的全屏元素显示其内容,在Flutter中,这些元素称为路由,它们由导航器(Navigator)控件管理。导航器管理一组路由(Route)对象,并提供了管理堆栈的方法,例如Navigator.push和Navigator.pop。
Future push(
BuildContext context,
Route route
)
// 将给定的路由添加到最靠近给定上下文的导航器的历史记录,并过渡到它
bool pop(
BuildContext context, [
result
])
// 从导航器中弹出最靠近给定上下文的路由
显示屏幕路由
虽然可以直接创建导航器,但最常见的是使用由WidgetsApp或MaterialApp控件创建的导航器,可以使用Navigator.of引用该导航器。
NavigatorState of(
BuildContext context
)
// 最接近该类的实例包含给定上下文的状态
一个MaterialApp是最简单的设置方式,MaterialApp的home成为导航器堆栈底部的路由。要在堆栈上推送(push)一 个新路由,您可以创建一个具有构建器功能的MaterialPageRoute实例,它可以创建您想要显示在屏幕上的任何内容。
MaterialPageRoute是一种模态路由,可以通过平台自适应过渡来切换屏幕。对于Android,页面推送过渡时向上滑动页面,并将其淡入淡出,弹出过渡则向下滑动页面,该过渡适应平台。在iOS上,页面从右侧滑入,反向弹出,当另一个页面进入以覆盖时,该页面也会在视差中向左移动。
默认情况下,当模态路由替换为另一路由时,上一个路由将保留在内存中,要在没有必要时释放所有资源,可以将maintainState设置为false。
现在我们新建一个项目myapp,然后用下面代码替换main.dart文件的代码:
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: new MyHomePage(title: '应用程序首页'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void _openNewPage() {
setState(() {
Navigator.of(context).push(new MaterialPageRoute<Null>(
builder: (BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text('新的页面')
),
body: new Center(
child: new Text(
'点击浮动按钮返回首页',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: () {
Navigator.of(context).pop();
},
child: new Icon(Icons.replay),
),
);
},
));
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Text(
'点击浮动按钮打开新页面',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _openNewPage,
child: new Icon(Icons.open_in_new),
),
);
}
}
通常没有必要提供一个使用Scaffold在路由中弹出导航器的控件,因为Scaffold会自动在其AppBar中添加一个后退按钮。按下后退按钮会导致Navigator.pop被调用,在Android上按系统后退按钮也是一样的。
使用命名导航器路由
移动应用程序经常管理大量路由,通常可以通过名称来引用它们。路由名称按惯例使用类似路径的结构,例如“/a/b/c”,应用程序的主页路由默认为“/”。
可以使用Map<String, WidgetBuilder>创建MaterialApp,该Map映射从路由的名称到将创建它的构建器,MaterialApp使用此映射为导航器的onGenerateRoute回调创建一个值。
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(title: '应用程序首页'),
routes: <String, WidgetBuilder> {
'/a': (BuildContext context) => new MyPage(title: 'A 页面'),
'/b': (BuildContext context) => new MyPage(title: 'B 页面'),
'/c': (BuildContext context) => new MyPage(title: 'C 页面')
},
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Row(
children: <Widget>[
new RaisedButton(
child: new Text('A按钮'),
onPressed: () { Navigator.of(context).pushNamed('/a'); },
),
new RaisedButton(
child: new Text('B按钮'),
onPressed: () { Navigator.of(context).pushNamed('/b'); },
),
new RaisedButton(
child: new Text('C按钮'),
onPressed: () { Navigator.of(context).pushNamed('/c'); },
)
]
)
);
}
}
class MyPage extends StatelessWidget {
MyPage({Key key, this.title}) : super(key: key);
final String title;
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(title)
),
);
}
}
Map<String, WidgetBuilder> routes是应用程序的顶级路由表,当使用Navigator.pushNamed推送命名路由时,将在此映射中查找路由名称。如果名称存在,则相关联的WidgetBuilder将用于构造一个MaterialPageRoute,该新的路由执行适当的过渡。
如果应用程序只有一个页面,那么可以使用home指定它,如果指定了home,那么在此映射中为Navigator.defaultRouteName提供路由是一个错误。如果请求没有在此表中指定的路由(或通过home),则会调用onGenerateRoute回调来构建页面。
路由可以返回一个值
当路由被推送,用于获取用户的输入时,可以通过pop方法的result参数返回用户的输入值。推送路由的方法返回Future类型,Future将在路由弹出时解析,而Future的值是pop方法的result参数。
例如,如果我们要求用户按“确定”确认操作,我们可以等待Navigator.push的结果:
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(title: '应用程序首页'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _result = false;
Future<Null> _openNewPage() async {
bool value = await Navigator.of(context).push(new MaterialPageRoute<bool>(
builder: (BuildContext context) {
return new Center(
child: new GestureDetector(
child: new Text("确定"),
onTap: () { Navigator.of(context).pop(true); },
),
);
}
));
setState(() {
_result = value;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Text(
'用户当前选择为 $_result',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _openNewPage,
child: new Icon(Icons.open_in_new),
),
);
}
}
如果用户按“确定”,则值将为真。如果用户退出路由,例如通过按Scaffold的后退按钮,该值将为null。当使用路由return值时,路由的type参数必须与pop的结果类型相匹配。这就是为什么我们使用MaterialPageRoute
弹出路由
路由不一定需要掩盖整个屏幕,PopupRoutes覆盖屏幕,屏幕颜色只能部分不透明,以允许当前屏幕显示。弹出路由是模态的,因为它们阻止了下面的控件的输入。
框架有创建和显示弹出路由的功能,例如:showDialog、showMenu和showModalBottomSheet。这些功能如上所述返回其推送的路由的Future,调用时可以等待返回的值在路由弹出时采取行动,或者发现路由的值。
还有一些创建弹出路由的控件,如PopupMenuButton和DropdownButton。这些控件创建PopupRoute的内部子类,并使用路由的push和pop方法来显示和关闭它们。
import 'package:flutter/material.dart';
import 'dart:async';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(title: '应用程序首页'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
bool _result = false;
Future<Null> _openNewPage() async {
bool value = await showDialog(
context: context,
barrierDismissible: true,
child: new Center(
child: new GestureDetector(
child: new Text("确定"),
onTap: () { Navigator.of(context).pop(true); },
),
)
);
setState(() {
_result = value;
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Text(
'用户当前选择为 $_result',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _openNewPage,
child: new Icon(Icons.open_in_new),
),
);
}
}
定制路由
您可以创建自己的控件库路由类,比如PopupRoute、ModalRoute或PageRoute的子类,以控制用于显示路由,包括路由模态屏障的颜色和行为以及路由其他方面的动画转换。
PageRouteBuilder类可以根据回调来定义自定义路由,下面是一个示例,当路由出现或消失时,它会旋转和淡化其子对象。此路由不会遮蔽整个屏幕,因为它指定了opaque: false,就像弹出路由一样。
import 'package:flutter/material.dart';
void main() {
runApp(new MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: new MyHomePage(title: '应用程序首页'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
void _openNewPage() {
Navigator.of(context).push(new PageRouteBuilder(
opaque: false,
pageBuilder: (BuildContext context, _, __) {
return new Center(child: new Text('定制页面路由'));
},
transitionsBuilder: (_, Animation<double> animation, __, Widget child) {
return new FadeTransition(
opacity: animation,
child: new RotationTransition(
turns: new Tween<double>(begin: 0.5, end: 1.0).animate(animation),
child: child,
),
);
}
));
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Text(
'点击浮动按钮打开定制页面',
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _openNewPage,
child: new Icon(Icons.open_in_new),
),
);
}
}
页面路由分为两部分:“页面”和“过渡”。页面成为传递给buildTransitions方法的子代的后代,通常,页面仅构建一次,因为它不依赖于其动画参数,在此示例中为_和__。过渡建立在每个帧的持续时间。