Flutter 入門指北(Part 6) 之路由

kukyxs發表於2019-04-14

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

上一節擼了個介面,雖然比較簡單,但是把前面講的知識串聯了下,但是介面之間的跳轉一直沒說,這節就講下 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

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

Navigator_push.png

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 在堆疊中的位置。那麼堆疊的變化圖就是這樣的

Navigator_pushReplacement.png

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 直接退出了,退出了,退出了,那麼堆疊變化如圖

Navigator_pushAndRemoveUnit1.png

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 了,所以...堆疊的變化圖如下

Navigator_pushAndRemoveUnit2.png

######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,那麼堆疊的變化很明顯了,如圖

Navigator_pop.png

popUntil

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

Navigator.popUntil(context, ModalRoute.withName('/'));
複製程式碼

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

Navigator_popUntil.png

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,先謝過,你的認可就是支援我繼續寫下去的動力~

相關文章