参考 book.flutterchina.club/chapter2/fl…
1. 示例
路由(Route
)在移动开发中通常指页面(Page
),在Android
中通常指一个Activity
。所谓路由管理,就是管理页面之间如何跳转,通常也可被称为导航管理。这和原生开发类似,无论是Android
还是iOS
,导航管理都会维护一个路由栈,路由入栈(push
)操作对应打开一个新页面,路由出栈(pop
)操作对应页面关闭操作,而路由管理主要是指如何来管理路由栈。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: MainRoute(),
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//导航到新路由
Navigator.push(context, MaterialPageRoute(builder: (context) {
return SecondRoute();
}));
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}
复制代码
MaterialPageRoute
继承自PageRoute
类,PageRoute
类是一个抽象类,表示占有整个屏幕空间的一个模态路由页面,它还定义了路由构建及切换时过渡动画的相关接口及属性。MaterialPageRoute
是Material
组件库的一个Widget
,它可以针对不同平台,实现与平台页面切换动画风格一致的路由切换动画:
对于Android
,当打开新页面时,新的页面会从屏幕底部滑动到屏幕顶部;当关闭页面时,当前页面会从屏幕顶部滑动到屏幕底部后消失,同时上一个页面会显示到屏幕上。 对于iOS
,当打开页面时,新的页面会从屏幕右侧边缘一致滑动到屏幕左边,直到新页面全部显示到屏幕上,而上一个页面则会从当前屏幕滑动到屏幕左侧而消失;当关闭页面时,正好相反,当前页面会从屏幕右侧滑出,同时上一个页面会从屏幕左侧滑入。 下面我们介绍一下MaterialPageRoute
构造函数的各个参数的意义:
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
复制代码
builder
是一个WidgetBuilder
类型的回调函数,它的作用是构建路由页面的具体内容,返回值是一个widget
。我们通常要实现此回调,返回新路由的实例。
settings
包含路由的配置信息,如路由名称、是否初始路由(首页)。
maintainState
:默认情况下,当入栈一个新路由时,原来的路由仍然会被保存在内存中,如果想在路由没用的时候释放其所占用的所有资源,可以设置maintainState
为false
。
fullscreenDialog
表示新的路由页面是否是一个全屏的模态对话框,在iOS
中,如果fullscreenDialog
为true
,新页面将会从屏幕底部滑入(而不是水平方向)。
2. Navigator
Navigator
是一个路由管理的widget
,它通过一个栈来管理一个路由widget
集合。通常当前屏幕显示的页面就是栈顶的路由。Navigator
提供了一系列方法来管理路由栈,在此我们只介绍其最常用的两个方法:
1. Future push(BuildContext context, Route route)
将给定的路由入栈(即打开新的页面),返回值是一个Future对象,用以接收新路由出栈(即关闭)时的返回数据。
2. bool pop(BuildContext context, [ result ])
将栈顶路由出栈,result
为页面关闭时返回给上一个页面的数据。
Navigator
还有很多其它方法,如Navigator.replace
、Navigator.popUntil
等,详情请参考API文档或SDK源码注释,在此不再赘述。
3. 实例方法
Navigator
类中第一个参数为context
的静态方法都对应一个Navigator
的实例方法, 比如Navigator.push(BuildContext context, Route route)
等价于Navigator.of(context).push(Route route)
,下面命名路由相关的方法也是一样的。
3. 命名路由
命名路由(Named Route
)即给路由起一个名字,然后可以通过路由名字直接打开新的路由。这为路由管理带来了一种直观、简单的方式。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
//home: MainRoute(),
//注册路由表
routes: {
/// '/'是特殊地址,第一个页面
"/" :(context) => MainRoute(),
"new_page": (context) => SecondRoute(),
},
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () async {
//导航到新路由
var result = await Navigator.pushNamed(context, "new_page");
debugPrint("返回:$result");
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context, "结束");
},
child: Text("返回"),
)
],
),
);
}
}
复制代码
命名路由的最大优点是直观,我们可以通过语义化的字符串来管理路由。
但其有一个明显的缺点:不能直接传递路由参数。假设SecondRoute
,需要接受一个字符串参数tip
,然后再在屏幕中心将tip
的内容显示出来。因为命名路由需要提前注册到路由表中,所以就无法动态修改tip
参数。但是后面的版本已经支持了参数,看下面
命名路由参数
在Flutter最初的版本中,命名路由是不能传递参数的,后来才支持了参数;下面展示命名路由如何传递并获取路由参数:
我们先注册一个路由:
routes:{
"new_page": (context) => SecondRoute(),
} ,
复制代码
在打开路由时传递参数
Navigator.of(context).pushNamed("new_page", arguments: "hi");
复制代码
在路由页通过RouteSetting对象获取路由参数:
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
//获取路由参数
var args=ModalRoute.of(context).settings.arguments
//...省略无关代码
}
}
复制代码
2. 自定义路由切换动画
Material
库中提供了MaterialPageRoute
,它在Android
上会上下滑动切换。如果想自定义路由切换动画,可以使用PageRouteBuilder
。
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: MainRoute(),
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () async {
//导航到新路由
var result = await Navigator.push(
context,
PageRouteBuilder(
///动画时间
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
///平移
return SlideTransition(
///Tween:在补间动画中,定义开始点结束点
position: new Tween<Offset>(
begin: const Offset(1.0, 0.0),
end: const Offset(0.0, 0.0),
).animate(animation),
child: SecondRoute(),
);
},
),
);
debugPrint("返回:$result");
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context, "结束");
},
child: Text("返回"),
)
],
),
);
}
}
复制代码
同时我们也可以对动画进行组合
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: MainRoute(),
);
}
}
class MainRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () async {
//导航到新路由
var result = await Navigator.push(
context,
PageRouteBuilder(
///动画时间
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (BuildContext context, Animation animation,
Animation secondaryAnimation) {
///透明渐变与旋转
return new FadeTransition(
opacity: animation,
child: new RotationTransition(
turns: new Tween<double>(begin: 0.5, end: 1.0)
.animate(animation),
child: SecondRoute(),
),
);
},),
);
debugPrint("返回:$result");
},
child: Text("进入第二页"),
)
],
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context, "结束");
},
child: Text("返回"),
)
],
),
);
}
}
复制代码
3. 注意点
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
///Navigator.push内部其实就是 Navigator.of(context).push
Navigator.of(context).push(MaterialPageRoute(builder: (_){
return new SecondRoute();
}));
},
child: Text("进入第二页"),
)
],
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}
复制代码
这段代码运行会出现错误:
问题关键点在于Navigator operation requested with a context that does not include a Navigator.(导航操作请求使用了不包含Navigator的上下文context)
Navigator
实际上也是一个Widget,这个异常出现在Navigator.of(context)
路由器的获取上,而这句代码会从当前的context的父级一层层向上去查找一个Navigator
,我们当前传递的context就是MyApp,它的父级是root——UI根节点。Navigator
这个widget的并不是由root创建的,因此在root下一级的上下文中无法获得Navigator
。
在之前所有的路由案例中,我们的上下文是MainRoute,它的父级是MaterialApp。MaterialApp内部就会创建一个Navigator。
MaterialApp->_MaterialAppState->WidgetsApp->_WidgetsAppState
所以问题就在于,Navigator
需要通过MaterialApp或者它孩子的上下文。
1. 解决一
按照此笔记最开始的正常路由演示案例来进行修改。
2. 解决二
使用Builder
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new MaterialApp(
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
///
Builder(builder: (context){
return RaisedButton(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute(builder: (_){
return new SecondRoute();
}));
},
child: Text("进入第二页"),
);
})
],
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}
复制代码
使用Builder嵌套,Builder的参数可以看成一个回调,接收自身的context并返回布局配置。现在路由是从Builder的父亲开始查找啦,自然能找到Navigator。
3. 解决三
使用navigatorKey
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey();
@override
Widget build(BuildContext context) {
return new MaterialApp(
///指定路由器widget的key
navigatorKey: navigatorKey,
title: 'Flutter Demo',
home: Scaffold(
appBar: AppBar(
title: Text("主页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
///输出Navigator
debugPrint(navigatorKey.currentWidget.runtimeType.toString());
navigatorKey.currentState.push(MaterialPageRoute(builder: (_){
return new SecondRoute();
}));
},
child: Text("进入第二页"),
)
],
),
),
);
}
}
class SecondRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("第二页"),
),
body: Column(
children: <Widget>[
Text("第一个页面"),
RaisedButton(
onPressed: () {
//路由pop弹出
Navigator.pop(context);
},
child: Text("返回"),
)
],
),
);
}
}
复制代码
在创建Navigator
的时候,会给一个key,这个key可以看成一个Widget的id。这里的**_navigator就是我们指定的navigatorKey**(如果我们没指定,会给默认值的,所以不要疑惑不指定是不是就不创建Navigator
了)。而通过这个key,就能够获得这个Navigator
。直接获得了路由自然不需要再去查找了!
转载于:https://juejin.im/post/5d00f8a6f265da1bd260e511