这章来聊聊flutter的路由管理,也可以理解为页面导航,用来处理页面之间的跳转、参数传递、动画展示等功能。
路由导航主要由跳转和返回两个操作,跳转是调用Navigator的push相关方法,返回是调用Navigator的pop相关方法,可以理解为push是将一个页面推送到路由栈中,pop是将一个页面从栈中移出。
push相关
先看一下Navigator中push的相关方法:
push
Navigator.push(context,Route);
@optionalTypeArgs
static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
return Navigator.of(context).push(route);
}
该方法接受一个BuildContext和一个Route,context就不用说了,下面了解一下Route:
简单一点看,Route分为了页面路由(PageRoute)和窗口路由(PopupRoute),而PopupRoute的默认实现均为私有,就是说如果以后要用到的话需要我们自己去实现。PageRoute默认提供了三个公开的实现类:
- CupertinoPageRoute:Cupertino风格的默认实现。
- MaterialPageRoute:Material风格的默认实现。
- PageRouteBuilder:自定义PageRoute,比如一些动画效果。
示例代码:
Navigator.push(context,MaterialPageRoute(builder: (context) => Page2()));
另外push相关方法返回的都是一个Future,可以通过它来获取下一个页面的被pop时的返回值。
Navigator.push(context,MaterialPageRoute(builder: (context) => Page2()))
.then((value) {
print('page1 push $value');
});
pushReplacement
替换当前页面,并且当新页面动画执行完成之后,disposing前一个页面。
Navigator.pushReplacement(context,MaterialPageRoute(builder: (context) => Page3()));
源码:
@optionalTypeArgs
static Future<T> pushReplacement<T extends Object, TO extends Object>(BuildContext context, Route<T> newRoute, { TO result }) {
return Navigator.of(context).pushReplacement<T, TO>(newRoute, result: result);
}
前两个参数同push,第三个可选参数result表示的是这个页面的返回结果,如果设置的话,会返回给被替换的这个页面的前一个页面。
我们可以做这样一个操作:
- 在Page1调用push方法跳转到Page2,并监听结果
-
在Page2调用pushReplacement方法跳转Page3,并设置result
得到的日志如下:
I/flutter (14537): Page1 build
I/flutter (14537): Page1 push Page2
I/flutter (14537): Page2 build
I/flutter (14537): Page2 pushReplacement Page3 and result: Page2 result
I/flutter (14537): Page3 build
I/flutter (14537): Page1 push result: Page2 result
pushAndRemoveUntil
跳转到指定页面,并按顺序(从栈顶到栈底)移出之前的所有页面,直到predicate返回true。
@optionalTypeArgs
static Future<T> pushAndRemoveUntil<T extends Object>(BuildContext context, Route<T> newRoute, RoutePredicate predicate) {
return Navigator.of(context).pushAndRemoveUntil<T>(newRoute, predicate);
}
typedef RoutePredicate = bool Function(Route<dynamic> route);
比如我从Page2调用跳转pushAndRemoveUntil到Page3,同时指定predicate的条件为route.settings.name == "/",那么跳转到Page3后Page2将被移除,因为第一个页面的默认RouteSetting的name属性值为"/"。
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => Page3()),
(route) {
print('route:$route');
return route.settings.name == "/";
})
.then((value) {
print('Page2 pushAndRemoveUntil result: $value');
});
如果predicate的条件为route.settings.name != "/",那么任何一个页面都不会被移除,因为判断第一个前页面Page2的时候predicate已经返回true。
pushNamed、pushReplacementNamed、pushNamedAndRemoveUntil
三者分别对应push、pushReplacement、pushAndRemoveUntil,提供了一种命名路由跳转,并且在flutter新版本中增加了一个可选参数arguments,用于页面之间的传参。路由的名字将会传递给Navigator的onGenerateRoute回调,并将返回的路由推入Navigator栈(具体可见下面的传参部分)。
这里以pushNamed方法为例,首先声明一个路由列表:
const String PAGE_2 = "/page2";
final Map<String, WidgetBuilder> _routes = {
PAGE_2: (_) => Page2(),
};
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(routes: _routes, home: Page1());
}
}
跳转的时候直接调用pushNamed传入路由对应的键值即可:
Navigator.pushNamed(context, PAGE_2);
对于pushNamedAndRemoveUntil的predicate参数,可以直接使用ModalRoute.withName(name)来指定。
pop相关
还是先看一下pop的相关方法:
pop
从栈内移除最顶上的页面。
@optionalTypeArgs
static bool pop<T extends Object>(BuildContext context, [ T result ]) {
return Navigator.of(context).pop<T>(result);
}
可以接两个参数:
- context:上下文。
- result:即我们前面提到的返回给上一个页面的值。
popUntil
按顺序从栈内移除最顶上的页面,直到predicate返回true。predicate参数的含义可以参照上面的pushAndRemoveUntil。
static void popUntil(BuildContext context, RoutePredicate predicate) {
Navigator.of(context).popUntil(predicate);
}
popAndPushNamed
就是pop和pushNamed两个方法的组合。
@optionalTypeArgs
Future<T> popAndPushNamed<T extends Object, TO extends Object>(
String routeName, {
TO result,
Object arguments,
}) {
pop<TO>(result);
return pushNamed<T>(routeName, arguments: arguments);
}
页面传参
如果是非命名路由,即push系列方法,直接使用路由的构造函数传参即可:
Navigator.push(context, MaterialPageRoute(builder: (context) => Page2(arguments: arguments)));
如果是命名路由,之前是不可以传参的,新版本中增加了一个arguments参数,配合onGenerateRoute也可以传递参数,因为命名路由会将路由的名字传递给onGenerateRoute回调,并将产生的路由推入Navigator。
const String PAGE_2 = "/page2";
const String PAGE_3 = "/page3";
final Map<String, Function> _routes = {
PAGE_2: (context, {arguments}) => Page2(arguments: arguments),
PAGE_3: (context, {arguments}) => Page3(arguments: arguments),
};
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Page1(),
onGenerateRoute: (routeSetting) {
Function _routeGenerate = _routes[routeSetting.name];
if (_routeGenerate != null)
return MaterialPageRoute(
builder: (context) => _routeGenerate(context, arguments: routeSetting.arguments));
},
);
}
}
class Page2 extends StatelessWidget {
Map<String, Object> arguments;
Page2({this.arguments});
@override
Widget build(BuildContext context) {
print('Page2 build');
print('arguments:$arguments');
...
}
}
Navigator.pushNamed(context, PAGE_2,arguments: {"name":"lili"});
得到日志如下:
I/flutter (24397): Page1 pushNamed Page2
I/flutter (24397): Page2 build
I/flutter (24397): arguments:{name: lili}
切换动画
如果想自定义页面的切换效果,我们可以使用PageRouteBuilder来自定义路由。
PageRouteBuilder({
RouteSettings settings,
@required this.pageBuilder,
this.transitionsBuilder = _defaultTransitionsBuilder,
this.transitionDuration = const Duration(milliseconds: 300),
this.opaque = true,
this.barrierDismissible = false,
this.barrierColor,
this.barrierLabel,
this.maintainState = true,
}) : assert(pageBuilder != null),
assert(transitionsBuilder != null),
assert(barrierDismissible != null),
assert(maintainState != null),
assert(opaque != null),
super(settings: settings);
settings
路由相关设置,名字、参数、是否初始路由,如果为空,则会生成一个默认的。
Route({ RouteSettings settings }) : settings = settings ?? const RouteSettings();
pageBuilder
用来构建路由的主要内容。可以查看ModalRoute.buildPage方法来了解它的参数信息。
typedef RoutePageBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation);
- context:正在构建的路由的上下文。
- animation:路由的主变换动画,如果是进入,值从0.0逐渐变化到1.0;如果是退出,值从1.0逐渐变化到0.0。
- secondaryAnimation:路由的次变换动画。
transitionsBuilder
用于构建路由的变换效果。可以通过ModalRoute.buildTransitions方法来了解它的参数信息。
typedef RouteTransitionsBuilder = Widget Function(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child);
- context:正在构建的路由的上下文。
- animation:路由的主变换动画,如果是进入,值从0.0逐渐变化到1.0;如果是退出,值从1.0逐渐变化到0.0。
- secondaryAnimation:路由的次变换动画。当把一个新的路由push到栈顶时,原栈顶的路由的secondaryAnimation值从0.0变化到1.0;当栈顶路由被pop的时候,它下面的那个路由的secondaryAnimation值从1.0变化到0.0。
- child:页面的内容,即pageBuilder返回的widget。
transitionDuration
变换效果的持续时间。
opaque
是否不透明,默认为true,如果是不透明的话,路由变换完成之后,不会再构建位于该路由之下的路由,以节省资源。
barrierColor
模态屏障的颜色。如果为null,则屏障将是透明的。比如弹出一个对话框时,背景可以设置成灰暗的。注意Dialog也是一个路由。
Future<T> showGeneralDialog<T>({
@required BuildContext context,
@required RoutePageBuilder pageBuilder,
bool barrierDismissible,
String barrierLabel,
Color barrierColor,
Duration transitionDuration,
RouteTransitionsBuilder transitionBuilder,
}) {
assert(pageBuilder != null);
assert(!barrierDismissible || barrierLabel != null);
return Navigator.of(context, rootNavigator: true).push<T>(_DialogRoute<T>(
pageBuilder: pageBuilder,
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel,
barrierColor: barrierColor,
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
));
}
可以看到其实Dialog就是一个_DialogRoute。
barrierDismissible
点击屏障是否自动消失。
我们来弹出一个Dialog验证一下,设置屏障颜色为半透明红色,点击屏障自动消失:
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: "Dismiss",
barrierColor: Color.fromRGBO(255, 0, 0, 0.5),
transitionDuration: Duration(milliseconds: 300),
pageBuilder: (context, animation, secondaryAnimation) {
return AlertDialog(
title: Text("标题"),
);
});
maintainState
当路由为inactive状态时,是否需要在内存中保存路由状态。
示例
我们来做一个简单的旋转渐隐的动画效果。
Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, animation, secondaryAnimation) {
return Page2();
},
transitionsBuilder:
(context, animation, secondaryAnimation, child) {
return FadeTransition(
opacity: animation,
child: RotationTransition(
turns: Tween(begin: 0.0, end: 1.0)
.animate(animation),
child: child,
),
);
},
transitionDuration: Duration(milliseconds: 500)));
共享元素动画
做过android的对这个一定不陌生,这里提一下在flutter中的简单实现。
使用Hero包裹要共享的widget,并设置相同的tag。
Hero(
tag: "btnBack",
child: RaisedButton(
onPressed: () {
print('Page2 pop');
Navigator.pop(context);
},
child: Text("返回"),
)),
...
Hero(
tag: "btnBack",
child: RaisedButton(
onPressed: () {
print('Page3 pop');
Navigator.pop(context);
},
child: Text("返回"),
)),