Flutter進階:路由、路由棧詳解及案例分析

1. 路由初體驗

路由(Routes)是什麼?路由是屏幕或應用程序頁面的抽象。html

Flutter 使咱們可以優雅地管理路由主要依賴的是 Navigator(導航器)類。這是一個用於管理一組具備某種進出規則的頁面的 Widget,也就是說用它咱們可以實現各個頁面間有規律的切換。而這裏的規則即是在其內部維護的一個「 路由棧」。git

學習 Android 的同窗知道 Activity 的啓動模式能夠實現各類業務需求,iOS 中也有嵌套路由的功能,Flutter 做爲最有潛力的跨平臺框架固然要吸收衆家之精華,它固然徹底有能力實現原生的各類效果!github

咱們先嚐試實現一個小的功能。app

1.1 組件路由

當咱們第一次打開應用程序,出如今眼前的即是路由棧中的第一個也是最底部實例:框架

void main() {
  runApp(MaterialApp(home: Screen1()));
}
複製代碼

要在堆棧上推送新的實例,咱們能夠調用導航器 Navigator.push ,傳入當前 context 而且使用構建器函數建立 MaterialPageRoute 實例,該函數能夠建立您想要在屏幕上顯示的內容。 例如:less

new RaisedButton(
   onPressed:(){
   Navigator.push(context, MaterialPageRoute<void>(
      builder: (BuildContext context) {
        return Scaffold(
          appBar: AppBar(title: Text('My Page')),
          body: Center(
            child: FlatButton(
              child: Text('POP'),
              onPressed: () {
                Navigator.pop(context);
              },
            ),
          ),
        );
      },
    ));
   },
   child: new Text("Push to Screen 2"),
),
複製代碼

點擊執行上方操做,咱們將成功打開第二個頁面。異步

1.2 命名路由

在通常應用中,咱們用的最多的仍是命名路由,它是將應用中須要訪問的每一個頁面命名爲不重複的字符串,咱們即可以經過這個字符串來將該頁面實例推動路由。async

例如,'/ home' 表示 HomeScreen, '/ login' 表示 LoginScreen。 '/' 表示主頁面。 這裏的命名規範與 REST API 開發中的路由相似。 因此 '/' 一般表示的是咱們的根頁面。ide

請看下方案例:函數

new MaterialApp(
  home: new Screen1(),
  routes: <String, WidgetBuilder> {
    '/screen1': (BuildContext context) => new Screen1(),
    '/screen2' : (BuildContext context) => new Screen2(),
    '/screen3' : (BuildContext context) => new Screen3(),
    '/screen4' : (BuildContext context) => new Screen4()
  },
)
複製代碼

Screen1()、Screen2()等是每一個頁面的類名。

咱們一樣能夠實現前面的功能:

new RaisedButton(
   onPressed:(){
     Navigator.of(context).pushNamed('/screen2');
   },
   child: new Text("Push to Screen 2"),
),
複製代碼

或者:

new RaisedButton(
   onPressed:(){
     Navigator.pushNamed(context, "/screen2")
   },
   child: new Text("Push to Screen 2"),
),
複製代碼

一樣能夠實現上方效果。

1.3 Pop

實現上面兩種方法後,此時,路由棧中的狀況以下:

1_RKtC1MKJbjSfMjUlR-2K7g

如今,當咱們想要回退的到主屏幕時,咱們則須要使用 pop 方法從 Navigator 的堆棧中彈出 Routes。

Navigator.of(context).pop();
複製代碼

1_hq7qfAer0wCCSyIBKr7sfg

使用 Scaffold 時,一般不須要顯式彈出路徑,由於 Scaffold 會自動向其 AppBar 添加一個「後退」按鈕,按下時會調用 Navigator.pop()

在 Android 中,按下設備後退按鈕也會這樣作。可是,咱們也有可能須要將此方法用於其餘組件,例如在用戶單擊「取消」按鈕時彈出 AlertDialog。

這裏要注意的是:切勿用 push 代替 pop,有同窗說我在 Screen2 push Screen1 部照樣能實現這個功能嗎?其實否則啊,請看下圖:

1_Xsyo5c8s1JwO6f2OQ1nNEg

因此 push 只用於向棧中添加實例,pop 彈出實例!(特殊需求除外)

2. 詳解路由棧

前面,咱們已經知道如何簡單在路由棧中 push、pop 實例,然而,當遇到一些特殊的狀況,這顯然不能知足需求。學習 Android 的同窗知道 Activity 的各類啓動模式能夠完成相應需求,Flutter 固然也有相似的能夠解決各類業務需求的實現方式!

請看下面使用方法與案例分析。

2.1 pushReplacementNamed 與 popAndPushNamed

RaisedButton(
  onPressed: () {
    Navigator.pushReplacementNamed(context, "/screen4");
  },
  child: Text("pushReplacementNamed"),
),
RaisedButton(
  onPressed: () {
    Navigator.popAndPushNamed(context, "/screen4");
  },
  child: Text("popAndPushNamed"),
),
複製代碼

咱們在 Screen3 頁面使用 pushReplacementNamedpopAndPushNamed 方法 push 了 Screen4。

此時路由棧狀況以下:

1_cr77kgOgz7KRjwvMAVXoAg

Screen4 代替了 Screen3

pushReplacementNamedpopAndPushNamed 的區別在於: popAndPushNamed 可以執行 Screen2 彈出的動畫與 Screen3 推動的動畫而 pushReplacementNamed 僅顯示 Screen3 推動的動畫。

1_cr77kgOgz7KRjwvMAVXoAg

案例:

pushReplacementNamed:當用戶成功登陸而且如今在 HomeScreen 上時,您不但願用戶還可以返回到 LoginScreen。所以,登陸應徹底由首頁替換。另外一個例子是從 SplashScreen 轉到 HomeScreen。 它應該只顯示一次,用戶不能再從 HomeScreen 返回它。 在這種狀況下,因爲咱們要進入一個全新的屏幕,咱們可能須要藉助此方法。

popAndPushNamed:假設您正在有一個 Shopping 應用程序,該應用程序在 ProductsListScreen 中顯示產品列表,用戶能夠在 FiltersScreen 中應用過濾商品。 當用戶單擊「應用篩選」按鈕時,應彈出 FiltersScreen 並使用新的過濾器值推回到 ProductsListScreen。 這裏 popAndPushNamed 顯然更爲合適。

2.2 pushNamedAndRemoveUntil

用戶已經登錄進入 HomeScreen ,而後通過一系列操做回到配合只界面想要退出登陸,你不可以直接 Push 進入 LoginScreen 吧?你須要將以前路由中的實例所有刪除是的用戶不會在回到先前的路由中。

pushNamedAndRemoveUntil 可實現該功能:

Navigator.of(context).pushNamedAndRemoveUntil('/screen4', (Route<dynamic> route) => false);
複製代碼

這裏的 (Route<dynamic> route) => false 可以確保刪除先前全部實例。

Logging out removes all routes and takes user back to LoginScreen

如今又有一個需求:咱們不但願刪除先前全部實例,咱們只要求刪除指定個數的實例。

咱們有一個須要付款交易的購物應用。在應用程序中,一旦用戶完成了支付交易,就應該從堆棧中刪除全部與交易或購物車相關的頁面,而且用戶應該被帶到 PaymentConfirmationScreen ,單擊後退按鈕應該只將它們帶回到 ProductsListScreenHomeScreen

1_aaZxoLUbKdFPgiIkBAmw7w

Navigator.of(context).pushNamedAndRemoveUntil('/screen4', ModalRoute.withName('/screen1'));
複製代碼

經過代碼,咱們推送 Screen4 並刪除全部路由,直到 Screen1

1_D81iZF-BikxXJHak7_NkhA

popUntil

想象一下,咱們在應用程序中要填寫一系列信息,表單分佈在多個頁面中。假設須要填寫三個頁面的表單一步接着一步。 然而,在表單的第 3 部分,用戶取消了填寫表單。 用戶單擊取消而且應彈出全部以前與表單相關的頁面,而且應該將用戶帶回 HomeScreen 或者 DashboardScreen,這種狀況下數據屬於數據無效! 咱們不會在這裏推新任何新東西,只是回到之前的路由棧中。

1_qV7mF0Kow2zch-fjksmA_Q

Navigator.popUntil(context, ModalRoute.withName('/screen2'));
複製代碼

2.3 Popup routes(彈出路由)

路由不必定要遮擋整個屏幕。 PopupRoutes 使用 ModalRoute.barrierColor 覆蓋屏幕,ModalRoute.barrierColor 只能部分不透明以容許當前屏幕顯示。 彈出路由是「模態」的,由於它們阻止了對下面其餘組件的輸入。

有一些方法能夠建立和顯示這類彈出路由。 例如:showDialog,showMenu 和 showModalBottomSheet。 如上所述,這些函數返回其推送路由的 Future(異步數據,參考下面的數據部分)。 執行能夠等待返回的值在彈出路由時執行操做。

還有一些組件能夠建立彈出路由,如 PopupMenuButton 和 DropdownButton。 這些組件建立 PopupRoute 的內部子類,並使用 Navigator 的push 和 pop 方法來顯示和關閉它們。

2.4 自定義路由

您能夠建立本身的一個窗口z組件庫路由類(如 PopupRoute,ModalRoute 或 PageRoute)的子類,以控制用於顯示路徑的動畫過渡,路徑的模態屏障的顏色和行爲以及路徑的其餘各個特性。

PageRouteBuilder 類能夠根據回調定義自定義路由。 下面是一個在路由出現或消失時旋轉並淡化其子節點的示例。 此路由不會遮擋整個屏幕,由於它指定了opaque:false,就像彈出路由同樣。

Navigator.push(context, PageRouteBuilder(
  opaque: false,
  pageBuilder: (BuildContext context, _, __) {
    return Center(child: Text('My PageRoute'));
  },
  transitionsBuilder: (___, Animation<double> animation, ____, Widget child) {
    return FadeTransition(
      opacity: animation,
      child: RotationTransition(
        turns: Tween<double>(begin: 0.5, end: 1.0).animate(animation),
        child: child,
      ),
    );
  }
));
複製代碼

ezgif-3-14c32a6d8764

路由兩部分構成,「pageBuilder」和「transitionsBuilder」。

該頁面成爲傳遞給 buildTransitions 方法的子代的後代。 一般,頁面只構建一次,由於它不依賴於其動畫參數(在此示例中以_和__表示)。 過渡是創建在每一個幀的持續時間。

2.5 嵌套路由

一個應用程序可使用多個路由導航器。將一個導航器嵌套在另外一個導航器下方可用於建立「內部旅程」,例如選項卡式導航,用戶註冊,商店結賬或表明整個應用程序子部分的其餘獨立個體。

iOS應用程序的標準作法是使用選項卡式導航,其中每一個選項卡都維護本身的導航歷史記錄。所以,每一個選項卡都有本身的導航器,建立了一種「並行導航」。

除了選項卡的並行導航以外,還能夠啓動徹底覆蓋選項卡的全屏頁面。例如:入職流程或警報對話框。所以,必須存在位於選項卡導航上方的「根」導航器。所以,每一個選項卡的 Navigators 實際上都是嵌套在一個根導航器下面的Navigators。

用於選項卡式導航的嵌套導航器位於 WidgetApp 和 CupertinoTabView 中,所以在這種狀況下您無需擔憂嵌套的導航器,但它是使用嵌套導航器的真實示例。

如下示例演示瞭如何使用嵌套的 Navigator 來呈現獨立的用戶註冊過程。

儘管此示例使用兩個 Navigators 來演示嵌套的 Navigators,但僅使用一個 Navigato r就能夠得到相似的結果。

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      // ...some parameters omitted...
      // MaterialApp contains our top-level Navigator
      initialRoute: '/',
      routes: {
        '/': (BuildContext context) => HomePage(),
        '/signup': (BuildContext context) => SignUpPage(),
      },
    );
  }
}

class SignUpPage extends StatelessWidget {
 @override
 Widget build(BuildContext context) {
   // SignUpPage builds its own Navigator which ends up being a nested
   // Navigator in our app.
   return Navigator(
     initialRoute: 'signup/personal_info',
     onGenerateRoute: (RouteSettings settings) {
       WidgetBuilder builder;
       switch (settings.name) {
         case 'signup/personal_info':
           // Assume CollectPersonalInfoPage collects personal info and then
           // navigates to 'signup/choose_credentials'.
           builder = (BuildContext _) => CollectPersonalInfoPage();
           break;
         case 'signup/choose_credentials':
           // Assume ChooseCredentialsPage collects new credentials and then
           // invokes 'onSignupComplete()'.
           builder = (BuildContext _) => ChooseCredentialsPage(
             onSignupComplete: () {
               // Referencing Navigator.of(context) from here refers to the
               // top level Navigator because SignUpPage is above the
               // nested Navigator that it created. Therefore, this pop()
               // will pop the entire "sign up" journey and return to the
               // "/" route, AKA HomePage.
               Navigator.of(context).pop();
             },
           );
           break;
         default:
           throw Exception('Invalid route: ${settings.name}');
       }
       return MaterialPageRoute(builder: builder, settings: settings);
     },
   );
 }
}
複製代碼

Navigator.of 在給定 BuildContext 中最近的根 Navigator 上運行。 確保在預期的 Navigator 下面提供BuildContext,尤爲是在建立嵌套 Navigators 的大型構建方法中。 Builder 組件可用於訪問組件子樹中所需位置的 BuildContext。

3. 頁面間數據傳遞

3.1 數據傳遞

在上面的大多數示例中,咱們推送新路由時沒有發送數據,但在實際應用中這種狀況應用不多。 要發送數據,咱們將使用 Navigator 將新的 MaterialPageRoute 用咱們的數據推送到堆棧上(這裏是 userName

String userName = "John Doe";
Navigator.push(
    context,
    new MaterialPageRoute(
        builder: (BuildContext context) =>
        new Screen5(userName)));
複製代碼

要在 Screen5 中獲得數據,咱們只需在 Screen5 中添加一個參數化構造函數:

class Screen5 extends StatelessWidget {

  final String userName;
  Screen5(this.userName);
  @override
  Widget build(BuildContext context) {
  print(userName)
  ...
  }
}
複製代碼

這表示咱們不只可使用 MaterialPageRoute 做爲 push 方法,還可使用 pushReplacementpushAndPopUntil 等。基本上從咱們描述的上述方法中路由方法,第一個參數如今將採用 MaterialPageRoute 而不是 namedRouteString

3.2 數據返回

咱們可能還想重新頁面返回數據。 就像一個警報應用程序,併爲警報設置一個新音調,您將顯示一個帶有音頻音調選項列表的對話框。 顯然,一旦彈出對話框,您將須要所選的項目數據。 它能夠這樣實現:

new RaisedButton(onPressed: ()async{
  String value = await Navigator.push(context, new MaterialPageRoute<String>(
      builder: (BuildContext context) {
        return new Center(
          child: new GestureDetector(
              child: new Text('OK'),
              onTap: () { Navigator.pop(context, "Audio1"); }
          ),
        );
      }
  )
  );
  print(value);

},
  child: new Text("Return"),)
複製代碼

Screen4 中嘗試並檢查控制檯的打印值。

另請注意:當路由用於返回值時,路由的類型參數應與 pop 的結果類型匹配。 這裏咱們須要一個 String 數據,因此咱們使用了 MaterialPageRoute <String>。 不指定類型也不要緊。

4. 其餘效果解釋

4.1 maybePop

源碼:

static Future<bool> maybePop<T extends Object>(BuildContext context, [ T result ]) {
    return Navigator.of(context).maybePop<T>(result);
  }

@optionalTypeArgs
  Future<bool> maybePop<T extends Object>([ T result ]) async {
    final Route<T> route = _history.last;
    assert(route._navigator == this);
    final RoutePopDisposition disposition = await route.willPop();
    if (disposition != RoutePopDisposition.bubble && mounted) {
      if (disposition == RoutePopDisposition.pop)
        pop(result);
      return true;
    }
    return false;
  }
複製代碼

若是咱們在初始路由上而且有人錯誤地試圖彈出這個惟一頁面怎麼辦? 彈出堆棧中惟一的頁面將關閉您的應用程序,由於它後面已經沒有頁面了。這顯然是很差的體驗。 這就是 maybePop() 起的做用。 點擊 Screen1 上的 maybePop 按鈕,沒有任何效果。 在 Screen3 上嘗試相同的操做,能夠正常彈出。

這種效果也可經過 canPop 實現:

4.2 canPop

源碼:

static bool canPop(BuildContext context) {
    final NavigatorState navigator = Navigator.of(context, nullOk: true);
    return navigator != null && navigator.canPop();
  }

bool canPop() {
    assert(_history.isNotEmpty);
    return _history.length > 1 || _history[0].willHandlePopInternally;
  }
複製代碼

若是佔中實例大於 1 或 willHandlePopInternally 屬性爲 true 返回 true,不然返回 false。

咱們能夠經過判斷 canPop 來肯定是否可以彈出該頁面。

4.3 如何去除默認返回按鈕

AppBar({
    Key key,
    this.leading,
    this.automaticallyImplyLeading = true,
    this.title,
    this.actions,
    this.flexibleSpace,
    this.bottom,
    this.elevation = 4.0,
    this.backgroundColor,
    this.brightness,
    this.iconTheme,
    this.textTheme,
    this.primary = true,
    this.centerTitle,
    this.titleSpacing = NavigationToolbar.kMiddleSpacing,
    this.toolbarOpacity = 1.0,
    this.bottomOpacity = 1.0,
  }) : assert(automaticallyImplyLeading != null),
       assert(elevation != null),
       assert(primary != null),
       assert(titleSpacing != null),
       assert(toolbarOpacity != null),
       assert(bottomOpacity != null),
       preferredSize = Size.fromHeight(kToolbarHeight + (bottom?.preferredSize?.height ?? 0.0)),
       super(key: key);
複製代碼

automaticallyImplyLeading置爲 false

5. 參考連接

docs.flutter.io/flutter/wid…

部分演示圖片來自:medium.com/flutter-community/flutter-push-pop-push-1bb718b13c31

個人 Github:github.com/MeandNi

個人博客:meandni.com/

相關文章
相關標籤/搜索