Flutter入門指北(Part6)之路由

NO IMAGE

該文已授權公眾號 「碼個蛋」,轉載請指明出處

上一節擼了個界面,雖然比較簡單,但是把前面講的知識串聯了下,但是界面之間的跳轉一直沒說,這節就講下 Flutter 中的「路由」來管理界面。

Navigator

Flutter 通過 Navigator 來進行頁面之間的跳轉,分為 push 系列和 pop 系列操作,帶 push 方法為入棧操作,帶 pop 方法為出棧操作。Navigatorpush 方法分兩類,一類是帶 Name 的,需要在 MaterialApp 下將 routers 屬性進行註冊,否則將會找不到該路由,還有一個是不帶 Name 的,可以通過 Router 直接跳轉。

說那麼多相信還不如直接上代碼和圖來的更直接。因為需要展示所有的跳轉至少需要 3 個頁面,所以我們創建最簡單的三個界面,通過文字來區別不同的頁面,因為需要調用帶有 Name 的方法,所以需要先在 MaterialApp 對路由進行註冊。

class DemoApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Learning Demo',
// 在這裡註冊路由,關聯 name 和界面
// '/' 表示根頁面,也就是 home 所對應的頁面,這邊就不需要配置 home 屬性了
routes: {'/': (_) => APage(), '/page_b': (_) => BPage(), '/page_c': (_) => CPage()},
debugShowCheckedModeBanner: false,
);
}
}
/// Page A,Button 的跳轉事件等會進行修改,目前先空著
class APage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Page A'),
),
body: Center(child: RaisedButton(onPressed: () {}, child: Text('To Page B'))),
);
}
}
/// Page B
class BPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Page B'),
),
body: Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
RaisedButton(onPressed: () {}, child: Text('To Page C')),
RaisedButton(onPressed: () {}, child: Text('Back Page A'))
])),
);
}
}
/// Page C
class CPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Page C'),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[RaisedButton(onPressed: () {}, child: Text('Back Last Page'))])),
);
}
}
push / pushNamed 方式跳轉

我們在 APageRaiseButtononPressed 方法加入如下代碼

Navigator.push(context, MaterialPageRoute(builder: (_) => BPage()));

或者

Navigator.pushNamed(context, '/page_b');

效果相同。跳轉後,可以發現,在 BPageAppBar 上有個返回按鈕,點擊可以返回 APage ,那麼也就是說通過 push 或者 pushNamed 方式跳轉的時候,界面堆棧的變化是直接在原來的堆棧上添加一個新的 page

為了凸顯堆棧的變化,所以繪製的圖中,會比使用的實際頁面多一個,下圖同

Flutter入門指北(Part6)之路由

pushReplacement / pushReplacementNamed / popAndPushNamed

APage 中的跳轉方式進行替換

Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => BPage()));

或者

Navigator.pushReplacementNamed(context, '/page_b');

或者

// 如果是第一個界面跳轉到下個界面,勿用,`BPage` 會顯示返回按鈕,但是點擊後,界面會變黑
// 因為 `APage` 已經不在堆棧中了,點擊後堆棧就沒有 `Page` 了,所以界面變黑
Navigator.popAndPushNamed(context, '/page_b');

效果相同,跳轉後,可以發現 BPage 的返回按鈕消失了,消失了,消失了,我們可以試下點擊返回按鍵,發現 App 直接退出了,也就是說,BPage 替代了 APage 在堆棧中的位置。那麼堆棧的變化圖就是這樣的

Flutter入門指北(Part6)之路由

pushAndRemoveUntil / pushNamedAndRemoveUntil
CASE 1

這個跳轉方式需要通過 CPage 來協助完成,將 APage 的跳轉方式修改為 push 方式,然後在 BPage 的第一個按鈕加入如下代碼

Navigator.pushAndRemoveUntil(context, 
MaterialPageRoute(builder: (_) => CPage()), (Route router) => false);

或者

Navigator.pushNamedAndRemoveUntil(context, '/page_c', (Route router) => false);

效果相同,點擊 BPage 的跳轉 CPage 按鈕後,界面來到 CPage,然後發現還是沒有返回按鈕,沒有返回按鈕,沒有返回按鈕,點擊下返回按鍵,然後發現 App 直接退出了,退出了,退出了,那麼堆棧變化如圖

Flutter入門指北(Part6)之路由

CASE 2

你以為這兩個方法只是為了把堆棧都清空嗎,那就太圖樣圖森破了,這邊展示另一種。修改跳轉的代碼

Navigator.pushAndRemoveUntil(context, 
MaterialPageRoute(builder: (_) => CPage()), ModalRoute.withName('/'));

或者

Navigator.pushNamedAndRemoveUntil(context, '/page_c', ModalRoute.withName('/'));

點擊跳轉 CPage 以後,發現返回按鈕又回來了…就這麼回來了…只是修改了一個參數,點擊返回按鈕,又回到了 APage,你可以在 APage 跳轉 BPage 中加入DPage EPage 等等更多的界面,只要保證 BPage 跳轉 CPage 的方式不變,點擊 CPage 的返回按鈕,又回到 APage 了,所以…堆棧的變化圖如下

Flutter入門指北(Part6)之路由

######SUMMARY

為什麼會這樣變化呢,還記得在 MaterialApp 中註冊的 router 麼,APagename 對應的為 '/',也就是說,該方法會把堆棧中在 ModalRoute.withName 所對應的 page 上的所有都 pop 出堆棧,如果把參數換成 /page_b,然後在跳轉 CPage 之前加入更多的界面,點擊 CPage 的返回按鈕,就會回到 BPage

QUESTION

這邊再提個小問題,有頁面 A,B,C,D,其路由的 name 分別為 ‘/’,’page_b’,’page_c’,’page_d’,啟動順序為 A -> B -> C -> C -> D,那麼在 D 頁面使用

Navigator.pushNamedAndRemoveUntil(context, '/page_c', ModalRoute.withName('/page_c'));

那麼堆棧最後剩下的頁面是 ABCC 還是ABC 呢?答案會在最後公佈,小夥伴可以先自己嘗試著實現。

pop

BPage 的第二個按鈕中加入 pop 操作

Navigator.pop(context);

跳轉到 BPage 後點擊該按鈕,界面回到 APage,那麼堆棧的變化很明顯了,如圖

Flutter入門指北(Part6)之路由

popUntil

這個方法還需要藉助 CPage ,在 CPage 的按鈕中加入

Navigator.popUntil(context, ModalRoute.withName('/'));

點擊返回按鈕,界面跳過 BPage 回到了 APage,解釋同 pushAndRemoveUntil 那麼堆棧的變化也顯而易見咯

Flutter入門指北(Part6)之路由

Navigator 傳值

######CASE 1 傳值給下個界面

修改下 BPageAPage 的按鈕點擊事件

class BPage extends StatelessWidget {
final String message;
BPage({Key key, @required this.message}) : super(key: key);
@override
Widget build(BuildContext context) {
print('passed value: $message');
return Scaffold(
// 省略相同代碼
);
}
}
// APage 跳轉事件
Navigator.push(context, MaterialPageRoute(builder: 
(_) => BPage(message: 'Message From Page A')));

點擊 APage 可以查看控制檯有輸出

2019-03-17 00:04:06.854 12868-12888/com.kuky.demo.flutterartsdemosapp I/flutter: passed value: Message From Page A

也就是成功把值傳遞過來了。但是,需要傳遞參數的話,之前在 MaterialApp 下注冊的路由就需要去除了

######CASE 2 傳值給上個界面

這邊可以查看下 pop 方法

@optionalTypeArgs
// pop 可以傳入一個可選參數 result,這個 result 也就是回傳給上個頁面的參數值了
static bool pop<T extends Object>(BuildContext context, [ T result ]) {
return Navigator.of(context).pop<T>(result);
}

既然知道 pop 如何傳遞值給上個界面,那麼如何在上個界面接收這個參數呢,還是看下 push 方法

@optionalTypeArgs
static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
return Navigator.of(context).push(route);
}
///
@optionalTypeArgs
Future<T> push<T extends Object>(Route<T> route) {
// ...省略無關代碼
// 這邊返回一個 Future 值,`pop` 所傳遞的值會在這邊返回
return route.popped;
}
/// The future completes with the value given to [Navigator.pop], if any.
Future<T> get popped => _popCompleter.future;

官方的註釋非常明白的指出,會在 Future 中攜帶 pop 傳遞的參數,那麼我們對 APage 跳轉 BPage 以及 BPage 返回 APage 的邏輯進行修改

/// APage
Navigator.push(context, MaterialPageRoute(builder: (_) 
=> BPage(message: 'Message From Page A')))
.then((value) => print('BACK MESSAGE => $value'));
/// BPage
Navigator.pop(context, 'Message back to PageA From BPage');

點擊返回後,能夠在控制檯發現有如下輸入

2019-03-17 16:35:53.820 13417-13442/com.kuky.demo.flutterartsdemosapp I/flutter: BACK MESSAGE => Message back to PageA From BPage

上個頁面成功接收到下個頁面回傳的數據。

CASE 3 通過系統返回按鈕傳值

CASE 2 情況下,通過按鈕對返回事件進行監聽,那加入我們需求沒有這個按鈕,只能通過系統默認的返回按鈕,或者物理返回按鍵,那該如何傳值呢,這裡就需要用 WillpopScope 對系統的返回按鈕進行監聽。我們對 CPage 做下修改,在 Scaffold 外面包裹一個 WillpopScope

class CPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return WillPopScope(
child: Scaffold(
appBar: AppBar(
title: Text('Page C'),
),
body: Center(
child: Column(mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[
RaisedButton(
onPressed: () {
Navigator.popUntil(context, ModalRoute.withName('/'));
},
child: Text('Back Last Page'))
])),
),
// 這裡對系統返回按鈕做監聽..
// 如果返回的是 `true` 則相當於 `pop` 操作,返回 `false` 則只執行上一步的 `pop` 操作
// 例如雙擊返回退出,也是通過 `WillpopScope` 來進行監聽
onWillPop: () async {
Navigator.pop(context, 'Hello~');
return false;
});
}
}

通過返回按鈕,BPage 會成功收到從 CPage 返回的 Hello~

以上代碼查看 router_main.dart 文件

路由切換動畫

假如說我們不想用系統自帶的切換動畫,需要弄一些比較酷炫的效果該怎麼辦,那就需要用到自定義路由切換動畫了。直接修改 BPage 跳轉 CPage 的代碼

Navigator.push(
context,
PageRouteBuilder(
// 返回目標頁面
pageBuilder: (context, anim, _) => CPage(),
// 切換動畫的切換時長
transitionDuration: Duration(milliseconds: 500),
// 切換動畫的切換效果,系統自帶的常用 Transition
// ScaleTransition: 縮放  SlideTransition: 滑動
// RotationTransition: 旋轉  FadeTransition: 透明度
transitionsBuilder: (context, anim, _, child) => ScaleTransition(
// Tween 是 flutter 的補間動畫,等講到動畫的時候再提吧,這邊先記住這麼使用
scale: Tween(begin: 0.0, end: 1.0).animate(anim),
// 這個值必須記得要傳,否則會不顯示界面
child: child,
)));

當再次點擊跳轉的時候,切換的動畫就有開始自帶的平滑效果變成縮放效果了。那如果要實現多個動畫呢,例如邊縮放,邊改變透明度,也很容易實現,只需要將 child 替換成 Transition 即可

Navigator.push(
context,
PageRouteBuilder(
pageBuilder: (context, anim, _) => CPage(),
transitionDuration: Duration(milliseconds: 500),
transitionsBuilder: (context, anim, _, child) => ScaleTransition(
scale: Tween(begin: 0.0, end: 1.0).animate(anim),
// 替換即可,如果要加入更多的動畫,替換 `child` 屬性就可以了
child: FadeTransition(
opacity: Tween(begin: 0.0, end: 1.0).animate(anim),
child: child,
),
)));

當然,為了方便重複利用,需要進行封裝,例如我們要封裝上面的縮放動畫效果

class ScalePageRoute extends PageRouteBuilder {
final Widget widget;
ScalePageRoute(this.widget)
: super(
transitionDuration: Duration(milliseconds: 500),
pageBuilder: (context, anim, _) => widget,
transitionsBuilder: (context, anim, _, child) => ScaleTransition(
scale: Tween(begin: 0.0, end: 1.0).animate(anim),
child: child,
));
}

然後直接在 Navigator 跳轉的時候調用該 Route 就可以了

該部分代碼查看 custom_routes.dart 文件

還記得我們之前寫的 demo 都是單個文件寫一個入口的嗎,現在我們就可以寫一個統一管理的頁面,對這些界面進行管理了,這個工作就交給大傢伙自己了,當然我也在源碼做了修改,可以查看 main.dart 文件

在前面有提出一個問題,這邊公佈下答案:堆棧中的頁面應該為 ABCC。你答對沒有呢~

最後代碼的地址還是要的:

  1. 文章中涉及的代碼:demos

  2. 基於郭神 cool weather 接口的一個項目,實現 BLoC 模式,實現狀態管理:flutter_weather

  3. 一個課程(當時買了想看下代碼規範的,代碼更新會比較慢,雖然是跟著課上的一些寫代碼,但是還是做了自己的修改,很多地方看著不舒服,然後就改成自己的實現方式了):flutter_shop

如果對你有幫助的話,記得給個 Star,先謝過,你的認可就是支持我繼續寫下去的動力~

相關文章

Flutter入門指北(Part11)之狀態管理,BLoC

Flutter入門指北(Part9)之彈窗和提示(SnackBar、BottomSheet、Dialog)

Flutter入門指北(Part8)之Sliver組件、NestedScrollView

Flutter入門指北(Part7)之滑動部件