一,概述
移動應用通常通過成為‘屏幕’或者‘頁面’的全屏元素顯示其內容,在Flutter中,這些元素統稱為路由,它們由導航器Navigator組件管理。導航器管理一組路由Route對象,並提供了管理堆棧的方法,例如Navigator.push和Navigator.pop。
如果類比這Ios記憶的話,你可以粗略地把一個路由對應到一個 UIViewController。Navigator的工作原理和 iOS 中 UINavigationController 非常相似,當你想跳轉到新頁面或者從新頁面返回時,它可以 push() 和 pop() 路由。
在Flutter中,有兩個主要的widget用於在頁面之間導航:
-
- Route是一個應用程序抽象的屏幕或頁面;
- Navigator 是一個管理路由的widget;
以上兩種widget對應Flutter中實現頁面導航的有兩種選擇:
-
- 靜態路由:具體指定一個由路由名構成的 Map。(MaterialApp)
- 動態路由:直接跳轉到一個路由。(WidgetApp)
二,頁面跳轉的兩個選擇
Flutter中,跳轉頁面有兩種方式:靜態路由(又可稱為命名路由)方式和動態路由(又可稱為組件路由)方式。在Flutter管理多個頁面有兩個核心概念和類:Route和Navigator。一個route是一個屏幕或者頁面的抽象,Navigator是管理route的Widget。Navigator可以通過route入棧和出棧來實現頁面之間的跳轉。
- 靜態路由(又稱命名路由,它是將應用中需要訪問的每個頁面命名為不重復的字符串,我們便可以通過這個字符串來將該頁面實例推進路由)
說明:'/ xxx' 表示 xxx頁面,'/' 表示主頁面。 這里的命名規范與 REST API 開發中的路由類似。 所以 '/' 通常表示的是我們的根頁面。
- 配置路由
在原頁面配置路由跳轉,就是在MaterialApp里設置每個route對應的頁面,注意:一個app只能有一個材料設計(MaterialApp),不然返回上一個頁面會黑屏。
代碼如下://入口頁面 class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( //靜態路由方式 配置初始路由 initialRoute: '/', routes: { //默認走這個條件`/` '/':(BuildContext context){ return HomeStateful(); }, //新頁面路由 '/mainnewroute':(BuildContext context){ return new newRoute(); } }, //主題色 theme: ThemeData( //設置為紅色 primarySwatch: Colors.red), //配置了初始路由,下面就不需要了 //home: HomeStateful(), ); } }
因為配置了初始路由,所以
home:HomeStateful
就不用配置了。 - 點擊跳轉
//如果新頁面不在同一個類中,記得把它導入 import 'mainnewroute.dart'; class HomeStateful extends StatefulWidget{ @override State<StatefulWidget> createState(){ return new HomeWidget(); } } class HomeWidget extends State<HomeStateful> { @override Widget build(BuildContext context) { ... //Pointer Widget TestContainer = Listener( child:Container( width: 300.0, height: 300.0, color:Colors.red, child: RaisedButton( child: Text('點擊我'), onPressed: (){ //頁面跳轉方法 Navigator.of(context).pushNamed('/mainnewroute'); }), ), ); return new Scaffold( appBar: new AppBar( title: new Text('Flutter Demo'), ), body:Center( child: TestContainer, ), ); } }
RaisedButton
配置了點擊方法,上面用了Navigator.of(context).pushNamed('/mainnewroute')
,執行到這句,路由會找routes
有沒有配置/mainnewroute
,有的話,就會根據配置跳到新的頁面。 - 配置新頁面
新頁面,我在lib
下建立一個新的文件(頁面)mainfourday.dart
,很簡單:
import 'package:flutter/material.dart'; class newRoute extends StatelessWidget{ @override Widget build(BuildContext context){ return HomeWidget(); //注意:不需要MaterialApp // return MaterialApp( // theme: ThemeData( // //設置為hongse // primarySwatch: Colors.red), // home: HomeWidget(), // ); } } class HomeWidget extends StatelessWidget{ @override Widget build(BuildContext context){ return Scaffold( appBar: AppBar( title: Text('new Route'), ), body: Center( child:RaisedButton( child: Text('返回'), onPressed: (){ //這是關閉頁面 Navigator.pop(context); }), // child: Text('這是新的頁面'), ), ); } }
最終效果如下:
- 配置路由
- 動態路由(又稱組件路由,要在堆棧上推送新的實例,我們可以調用導航器
Navigator.push
,傳入當前 context 並且使用構建器函數創建 MaterialPageRoute 實例,該函數可以創建您想要在屏幕上顯示的內容)
下面說一下跳轉頁面的第二種方式,動態路由方式:
child: RaisedButton( child: Text('點擊我'), onPressed: (){ //Navigator.of(context).pushNamed('/mainnewroute'); //動態路由 Navigator.push( context, MaterialPageRoute(builder: (newPage){ return new newRoute(); }), ); }),
代碼效果和上面是一樣的。
三,詳解路由棧
前面,我們已經知道如何簡單在路由棧中 push、pop 實例,然而,當遇到一些特殊的情況,這顯然不能滿足需求。Flutter 當然也有類似的可以解決各種業務需求的實現方式!
-
pushReplacementNamed 與 popAndPushNamed
假如我們定義了四個靜態路由,分別為"/Screen1","/Screen2","/Screen3","/Screen4"
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
顯然更為合適。
-
pushNamedAndRemoveUntil
用戶已經登陸進入
HomeScreen
,然后經過一系列操作回到配合界面想要退出登錄,你不能夠直接 Push 進入LoginScreen
吧?你需要將之前路由中的實例全部刪除,是的用戶不會在回到先前的路由中。
-
- pushNamedAndRemoveUntil 可實現該功能:
Navigator.of(context).pushNamedAndRemoveUntil('/screen4', (Route<dynamic> route) => false);
這里的
(Route<dynamic> route) => false
能夠確保刪除先前所有實例。 -
現在又有一個需求:我們不希望刪除先前所有實例,我們只要求刪除指定個數的實例。
- pushNamedAndRemoveUntil 可實現該功能:
我們有一個需要付款交易的購物應用。在應用程序中,一旦用戶完成了支付交易,就應該從堆棧中刪除所有與交易或購物車相關的頁面,並且用戶應該被帶到
PaymentConfirmationScreen
,單擊后退按鈕應該只將它們帶回到ProductsListScreen
或HomeScreen
。
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。
四,頁面跳轉發送數據
頁面跳轉時有時需要發送數據到第二個頁面,比如從訂單列表到商品詳情頁面時,通常需要發送商品ID參數。
- 動態路由傳遞參數
Navigator.push( context, MaterialPageRoute(builder: (newPage){ return new newRoute("這是一份數據到新頁面"); }), );
在新頁面改為如下:
import 'package:flutter/material.dart'; class newRoute extends StatelessWidget{ //接收上一個頁面傳遞的數據 String str; //構造函數 newRoute(this.str); @override Widget build(BuildContext context){ return HomeWidget(str); } } class HomeWidget extends StatelessWidget{ String newDate; HomeWidget(this.newDate); @override Widget build(BuildContext context){ return Scaffold( appBar: AppBar( title: Text('new Route'), ), body: Center( child:RaisedButton( //顯示上一個頁面所傳遞的數據 child: Text(newDate), onPressed: (){ Navigator.pop(context); }), // child: Text('這是新的頁面'), ), ); } }
- 靜態路由傳遞參數
靜態路由方式傳遞參數,也就是在newRoute()加上所要傳遞的參數就可以了
//新頁面路由 '/mainnewroute':(context){ return new newRoute("sdsd"); }
四,頁面跳轉返回數據
- 數據傳遞
在上面的大多數示例中,我們推送新路由時沒有發送數據,但在實際應用中這種情況應用很少。 要發送數據,我們將使用 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。
- 數據返回
我們可能還想從新頁面返回數據。 就像一個警報應用程序,並為警報設置一個新音調,您將顯示一個帶有音頻音調選項列表的對話框。 顯然,一旦彈出對話框,您將需要所選的項目數據。 它可以這樣實現:
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
如果我們在初始路由上並且有人錯誤地試圖彈出這個唯一頁面怎么辦? 彈出堆棧中唯一的頁面將關閉您的應用程序,因為它后面已經沒有頁面了。這顯然是不好的體驗。 這就是 maybePop() 起的作用。 點擊 Screen1 上的 maybePop 按鈕,沒有任何效果。 在 Screen3 上嘗試相同的操作,可以正常彈出。
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; } -
canPop(maybePop效果也可通過 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