這章來聊聊flutter的路由管理,也可以理解為頁面導航,用來處理頁面之間的跳轉、參數(shù)傳遞、動畫展示等功能。
路由導航主要由跳轉和返回兩個操作,跳轉是調用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的默認實現(xiàn)均為私有,就是說如果以后要用到的話需要我們自己去實現(xiàn)。PageRoute默認提供了三個公開的實現(xiàn)類:
- CupertinoPageRoute:Cupertino風格的默認實現(xiàn)。
- MaterialPageRoute:Material風格的默認實現(xiàn)。
- 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
替換當前頁面,并且當新頁面動畫執(zhí)行完成之后,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);
}
前兩個參數(shù)同push,第三個可選參數(shù)result表示的是這個頁面的返回結果,如果設置的話,會返回給被替換的這個頁面的前一個頁面。
我們可以做這樣一個操作:
- 在Page1調用push方法跳轉到Page2,并監(jiān)聽結果
-
在Page2調用pushReplacement方法跳轉Page3,并設置result
route_2.gif
得到的日志如下:
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
跳轉到指定頁面,并按順序(從棧頂?shù)綏5祝┮瞥鲋暗乃许撁?,直到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已經(jīng)返回true。

pushNamed、pushReplacementNamed、pushNamedAndRemoveUntil
三者分別對應push、pushReplacement、pushAndRemoveUntil,提供了一種命名路由跳轉,并且在flutter新版本中增加了一個可選參數(shù)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參數(shù),可以直接使用ModalRoute.withName(name)來指定。
pop相關
還是先看一下pop的相關方法:

pop
從棧內移除最頂上的頁面。
@optionalTypeArgs
static bool pop<T extends Object>(BuildContext context, [ T result ]) {
return Navigator.of(context).pop<T>(result);
}
可以接兩個參數(shù):
- context:上下文。
- result:即我們前面提到的返回給上一個頁面的值。
popUntil
按順序從棧內移除最頂上的頁面,直到predicate返回true。predicate參數(shù)的含義可以參照上面的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);
}
頁面?zhèn)鲄?/h1>
如果是非命名路由,即push系列方法,直接使用路由的構造函數(shù)傳參即可:
Navigator.push(context, MaterialPageRoute(builder: (context) => Page2(arguments: arguments)));
如果是命名路由,之前是不可以傳參的,新版本中增加了一個arguments參數(shù),配合onGenerateRoute也可以傳遞參數(shù),因為命名路由會將路由的名字傳遞給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
路由相關設置,名字、參數(shù)、是否初始路由,如果為空,則會生成一個默認的。
Route({ RouteSettings settings }) : settings = settings ?? const RouteSettings();
pageBuilder
用來構建路由的主要內容??梢圆榭碝odalRoute.buildPage方法來了解它的參數(shù)信息。
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方法來了解它的參數(shù)信息。
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到棧頂時,原棧頂?shù)穆酚傻膕econdaryAnimation值從0.0變化到1.0;當棧頂路由被pop的時候,它下面的那個路由的secondaryAnimation值從1.0變化到0.0。
- child:頁面的內容,即pageBuilder返回的widget。
transitionDuration
變換效果的持續(xù)時間。
opaque
是否不透明,默認為true,如果是不透明的話,路由變換完成之后,不會再構建位于該路由之下的路由,以節(jié)省資源。
barrierColor
模態(tài)屏障的顏色。如果為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狀態(tài)時,是否需要在內存中保存路由狀態(tài)。
示例
我們來做一個簡單的旋轉漸隱的動畫效果。
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中的簡單實現(xiàn)。
使用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("返回"),
)),

