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

路由初體驗

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

Flutter 使我們能夠優雅地管理路由主要依賴的是 Navigator(導航器)類。這是一個用於管理一組具有某種進出規則的組件的 Widget,也就是說用它我們能夠實現各個組件間有規律的切換。而這裏的規則便是在其內部維護的一個“ 路由棧”,我們先嚐試實現一個小的功能。

組件路由

當我們第一次打開應用程序,出現在眼前的便是路由棧中的第一個也是最底部實例:

void main() {
  runApp(MaterialApp(home: Screen1()));
}

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

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"),
),

點擊執行上方操作,我們將成功打開第二個頁面。

命名路由

在一般應用中,我們用的最多的還是命名路由,它是將應用中需要訪問的每個頁面命名爲不重複的字符串,我們便可以通過這個字符串來將改頁面實例推進路由。

例如,'/ home' 表示 HomeScreen, '/ login' 表示 LoginScreen。 '/' 表示主頁面。 這裏的命名規範與 REST API 開發中的路由類似。 所以 '/' 通常表示的是我們的根頁面。

請看下方案例:

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"),
),

Pop

實現上面兩種方法,此時,路由棧中的情況如下:

現在,當我們想要回退的到主屏幕時,我們則需要使用 pop 方法從 Navigator 的堆棧中彈出 Routes。

Navigator.of(context).pop();

使用 Scaffold 時,通常不需要顯式彈出路徑,因爲 Scaffold 會自動向其 AppBar 添加一個“後退”按鈕,按下時會調用 Navigator.pop()。 在 Android 中,按下設備後退按鈕也會這樣做。 但是,我們也可能需要將此方法用於其他用例,例如在用戶單擊“取消”按鈕時彈出 AlertDialog。

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

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

詳解路由棧

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

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

popAndPushNamed 與 popAndPushNamed

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

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

此時路由棧情況如下:

Screen3 代替了 Screen2。 pushReplacementNamedpopAndPushNamed 的區別在於 popAndPushNamed 能夠執行 Screen2 彈出的動畫與 Screen3 推進的動畫而 pushReplacementNamed 僅顯示 Page3 推進的動畫。

案例:

pushReplacementNamed

當用戶成功登錄並且現在在 HomeScreen 上時,您不希望用戶還能夠返回到 LoginScreen。因此,登錄應完全由首頁替換。另一個例子是從 SplashScreen 轉到 HomeScreen。 它應該只顯示一次,用戶不能再從 HomeScreen 返回它。 在這種情況下,由於我們要進入一個全新的屏幕,我們可能需要藉助此方法。

popAndPushNamed

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

pushNamedAndRemoveUntil

用戶已經登陸進入 HomeScreen ,然後經過一系列操作回到配合只界面想要退出登錄,你不能夠直接進入 LoginScreen 吧?你需要將之前路由中的實例全部刪除是的用戶不會在回到先前的路由中。

pushNamedAndRemoveUntil 可實現該功能:

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

這裏的 (Route<dynamic> route) => false 能夠確保刪除先前所有實例。

現在又有一個需求:我們不希望刪除先前所有實例,我們只要求刪除置頂個數的實例:

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

Navigator.of(context).pushNamedAndRemoveUntil('/screen4', ModalRoute.withName('/screen1'));

通過代碼,我們推送 Screen4 並刪除所有路由,直到 Screen1

popUntil

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

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

Popup routes(彈出路由)

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

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

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

自定義路由

您可以創建自己的一個窗口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,
      ),
    );
  }
));

路由兩部分構成,“pageBuilder”和“transitionsBuilder”。 該頁面成爲傳遞給 buildTransitions 方法的子代的後代。 通常,頁面只構建一次,因爲它不依賴於其動畫參數(在此示例中以_表示)。 過渡是建立在每個幀的持續時間。

嵌套路由

一個應用程序可以使用多個路由導航器。將一個導航器嵌套在另一個導航器下方可用於創建“內部旅程”,例如選項卡式導航,用戶註冊,商店結帳或代表整個應用程序子部分的其他獨立個體。

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。

頁面間數據傳遞

數據傳遞

在上面的大多數示例中,我們推送新路由時沒有發送數據,但在實際應用中這種情況應用很少。 要發送數據,我們將使用 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

數據返回

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

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>。 不指定類型也沒關係。

其他效果解釋

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 實現:

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 來確定是否能夠彈出該頁面。

如何去除默認返回按鈕

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

參考鏈接

https://docs.flutter.io/flutter/widgets/Navigator-class.html

https://medium.com/flutter-community/flutter-push-pop-push-1bb718b13c31持續

我的 Github:https://github.com/MeandNi

我的博客:https://meandni.com/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章