前面,我們已經知道如何簡單在路由棧中 push、pop 實例,然而,當遇到一些特殊的情況,這顯然不能滿足需求。學習 Android 的同學知道 Activity 的各種啟動模式可以完成相應需求,Flutter 當然也有類似的可以解決各種業務需求的實現方式!
請看下面使用方法與案例分析。
1.1 pushReplacementNamed 與 popAndPushNamed
RaisedButton( onPressed: () { Navigator.pushReplacementNamed(context, "/screen4"); }, child: Text("pushReplacementNamed"), ), RaisedButton( onPressed: () { Navigator.popAndPushNamed(context, "/screen4"); }, child: Text("popAndPushNamed"), ),
我們在 Screen3 頁面使用 pushReplacementNamed 與 popAndPushNamed 方法 push 了 Screen4。
此時路由棧情況如下:
Screen4 代替了 Screen3。
pushReplacementNamed 與 popAndPushNamed 的區別在於: popAndPushNamed 能夠執行 Screen2 彈出的動畫與 Screen3 推進的動畫而 pushReplacementNamed 僅顯示 Screen3 推進的動畫。
案例:
pushReplacementNamed:當用戶成功登錄並且現在在 HomeScreen 上時,您不希望用戶還能夠返回到 LoginScreen。因此,登錄應完全由首頁替換。另一個例子是從 SplashScreen 轉到 HomeScreen。 它應該只顯示一次,用戶不能再從 HomeScreen 返回它。 在這種情況下,由於我們要進入一個全新的屏幕,我們可能需要借助此方法。
popAndPushNamed:假設您正在有一個 Shopping 應用程序,該應用程序在 ProductsListScreen 中顯示產品列表,用戶可以在 FiltersScreen 中應用過濾商品。 當用戶單擊“應用篩選”按鈕時,應彈出 FiltersScreen 並使用新的過濾器值推回到 ProductsListScreen。 這里 popAndPushNamed 顯然更為合適。
1.2 pushReplacementNamed 與 popAndPushNamed區別
pushReplacement和pushReplacementNamed的功能一致,它們二者的區別與push和pushNamed的區別一樣——前者直接將頁面入棧,后者通過路由命名的名字將頁面入棧。這兩對的使用方法也一致,不同的是pushReplacement和pushReplacementNamed不是講新的頁面直接入棧,而是替換掉棧頂的頁面。類比Android原生可以理解為:在啟動新的Activity時,finish()掉當前頁面。
Navigator.of(context).pushReplacementNamed('/d');

1.3 pushNamedAndRemoveUntil
用戶已經登陸進入 HomeScreen ,然后經過一系列操作回到配合只界面想要退出登錄,你不能夠直接 Push 進入 LoginScreen 吧?你需要將之前路由中的實例全部刪除是的用戶不會在回到先前的路由中。
pushNamedAndRemoveUntil 可實現該功能:
Navigator.of(context).pushNamedAndRemoveUntil('/screen4', (Route<dynamic> route) => false);
這里的 (Route<dynamic> route) => false 能夠確保刪除先前所有實例。
現在又有一個需求:我們不希望刪除先前所有實例,我們只要求刪除指定個數的實例。
我們有一個需要付款交易的購物應用。在應用程序中,一旦用戶完成了支付交易,就應該從堆棧中刪除所有與交易或購物車相關的頁面,並且用戶應該被帶到 PaymentConfirmationScreen ,單擊后退按鈕應該只將它們帶回到 ProductsListScreen 或 HomeScreen。
Navigator.of(context).pushNamedAndRemoveUntil('/screen4', ModalRoute.withName('/screen1'));
通過代碼,我們推送 Screen4 並刪除所有路由,直到 Screen1:
1.4 pushAndRemoveUntil和pushNamedAndRemoveUntil區別
兩者的區別不在贅述,它們的實現的功能為:向棧添加新的路由,並刪除所有先前的路由,直到路由為指定的路由為止——例如在退出登錄時我們可以直接清除棧內的頁面返回首頁。用法如下:
Navigator.pushNamedAndRemoveUntil(context, "/a",ModalRoute.withName("/a"));
假如原來的頁面順序為a->b->c->d,執行完上述代碼后為:a->a。

removeRoute 和removeRouteBelow區別
removeRoute表示從Navigator中刪除路由,同時執行Route.dispose釋放Route自身資源,路由的生命周期結束。
removeRouteBelow從Navigator中刪除路由,同時執行Route.dispose操作,要替換的路由是傳入參數anchorRouter里面的路由。
replace 和replaceRouteBelow
replace將Navigator中的路由替換成一個新路由。 replaceRouteBelow 將Navigator中的路由替換成一個新路由,要替換的路由是是傳入參數anchorRouter里面的路由。
popUntil
popUntil作用是反復執行pop 直到返回到我們指定的頁面為止,popUntil接收一個函數,等到該函數的參數predicate返回true為結束pop操作:
Navigator.popUntil(context, ModalRoute.withName('/a'));
假如原來的頁面順序為a->b->c->d,執行完上述代碼后為:a。

該方法可以幫助我們清除棧內指定頁面之上所有的頁面,然后顯示指定頁面(例如快速回到主頁)。
1.3 Popup routes(彈出路由)
路由不一定要遮擋整個屏幕。 PopupRoutes 使用 ModalRoute.barrierColor 覆蓋屏幕,ModalRoute.barrierColor 只能部分不透明以允許當前屏幕顯示。 彈出路由是“模態”的,因為它們阻止了對下面其他組件的輸入。
有一些方法可以創建和顯示這類彈出路由。 例如showDialogshowMenu和showModalBottomSheet。 如上所述,這些函數返回其推送路由的 Future(異步數據,參考下面的數據部分)。 執行可以等待返回的值在彈出路由時執行操作。
還有一些組件可以創建彈出路由,如PopupMenuButton和DropdownButton。這些組件創建 PopupRoute 的內部子類,並使用 Navigator 的push 和 pop 方法來顯示和關閉它們。
1.4 自定義路由
您可以創建自己的一個窗口組件庫路由類(如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 方法的子代的后代。 通常,頁面只構建一次,因為它不依賴於其動畫參數(在此示例中以_和__表示)。 過渡是建立在每個幀的持續時間。
1.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。
2. 頁面間數據傳遞
2.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 方法,還可以使用 pushReplacement ,pushAndPopUntil 等。基本上從我們描述的上述方法中路由方法,第一個參數現在將采用 MaterialPageRoute 而不是 namedRoute 的 String。
2.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>。 不指定類型也沒關系。
3. 其他效果解釋
3.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 實現:
3.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 來確定是否能夠彈出該頁面。
3.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
