初探Flutter(三) 路由管理

語言: CN / TW / HK

highlight: tomorrow-night-bright

一、基本概念

所謂路由管理,就是管理頁面之間如何跳轉,通常也可被稱為導航管理。Flutter 中的路由管理和原生開發類似,無論是 Android 還是 iOS,導航管理都會維護一個路由棧,路由入棧(push)操作對應開啟一個新頁面,路由出棧(pop)操作對應頁面關閉操作,而路由管理主要是指如何來管理路由棧。

二、MaterialPageRoute

2.1 基本定義

MaterialPageRoute 是 Material元件庫提供的元件,它可以針對不同平臺,實現與平臺頁面切換動畫風格一致的路由切換動畫:

  • 對於 Android,當開啟新頁面時,新的頁面會從螢幕底部滑動到螢幕頂部;當關閉頁面時,當前頁面會從螢幕頂部滑動到螢幕底部後消失,同時上一個頁面會顯示到螢幕上。
  • 對於 iOS,當開啟頁面時,新的頁面會從螢幕右側邊緣一直滑動到螢幕左邊,直到新頁面全部顯示到螢幕上,而上一個頁面則會從當前螢幕滑動到螢幕左側而消失;當關閉頁面時,正好相反,當前頁面會從螢幕右側滑出,同時上一個頁面會從螢幕左側滑入。

dart MaterialPageRoute({ WidgetBuilder builder, RouteSettings settings, bool maintainState = true, bool fullscreenDialog = false, })

  • builder 是一個WidgetBuilder型別的回撥函式,它的作用是構建路由頁面的具體內容,返回值是一個widget。我們通常要實現此回撥,返回新路由的例項。
  • settings 包含路由的配置資訊,如路由名稱、是否初始路由(首頁)。
  • maintainState 預設情況下,當入棧一個新路由時,原來的路由仍然會被儲存在記憶體中,如果想在路由沒用的時候釋放其所佔用的所有資源,可以設定maintainStatefalse
  • fullscreenDialog 表示新的路由頁面是否是一個全屏的模態對話方塊,在 iOS 中,如果fullscreenDialog為 true,新頁面將會從螢幕底部滑入(而不是水平方向)。

2.2 使用例子

```dart class NewRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("New route"), ), body: Center( child: Text("This is new route"), ), ); } }

//...

Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ... //省略無關程式碼 TextButton( child: Text("open new route"), onPressed: () { //導航到新路由
Navigator.push( context, MaterialPageRoute(builder: (context) { return NewRoute(); }), ); }, ), ], ) ```

三、Navigator

3.1 基本定義

Navigator是一個路由管理的元件,它提供了開啟和退出路由頁方法。Navigator通過一個棧來管理活動路由集合。通常當前螢幕顯示的頁面就是棧頂的路由。Navigator提供了一系列方法來管理路由棧,在此我們只介紹其最常用的兩個方法:

  1. Future push(BuildContext context, Route route) 將給定的路由入棧(即開啟新的頁面),返回值是一個Future物件,用以接收新路由出棧(即關閉)時的返回資料。

  2. bool pop(BuildContext context, [ result ]) 將棧頂路由出棧,result 為頁面關閉時返回給上一個頁面的資料。

    Navigator 還有很多其他方法,如Navigator.replaceNavigator.popUntil等,詳情請參考API文件或SDK 原始碼註釋,在此不再贅述。下面我們還需要介紹一下路由相關的另一個概念“命名路由”。

  3. 例項方法 Navigator類中第一個引數為context的靜態方法都對應一個Navigator的例項方法, 比如Navigator.push(BuildContext context, Route route)等價於Navigator.of(context).push(Route route) ,下面命名路由相關的方法也是一樣的。

四、路由傳值

4.1 非命名路由傳值

```dart class TipRoute extends StatelessWidget { TipRoute({ Key key, required this.text, // 接收一個text引數 }) : super(key: key); final String text;

@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("提示"), ), body: Padding( padding: EdgeInsets.all(18), child: Center( child: Column( children: [ Text(text), ElevatedButton( onPressed: () => Navigator.pop(context, "我是返回值"), child: Text("返回"), ) ], ), ), ), ); } } ```

dart class RouterTestRoute extends StatelessWidget { @override Widget build(BuildContext context) { return Center( child: ElevatedButton( onPressed: () async { // 開啟`TipRoute`,並等待返回結果 var result = await Navigator.push( context, MaterialPageRoute( builder: (context) { return TipRoute( // 路由引數 text: "我是提示xxxx", ); }, ), ); //輸出`TipRoute`路由返回結果 print("路由返回值: $result"); }, child: Text("開啟提示頁"), ), ); } }

需要說明:

  1. 提示文案“我是提示xxxx”是通過TipRoutetext引數傳遞給新路由頁的。我們可以通過等待Navigator.push(…)返回的Future來獲取新路由的返回資料。

  2. TipRoute頁中有兩種方式可以返回到上一頁;第一種方式是直接點選導航欄返回箭頭,第二種方式是點選頁面中的“返回”按鈕。這兩種返回方式的區別是前者不會返回資料給上一個路由,而後者會。下面是分別點選頁面中的返回按鈕和導航欄返回箭頭後,RouterTestRoute頁中print方法在控制檯輸出的內容:

I/flutter (27896): 路由返回值: 我是返回值 I/flutter (27896): 路由返回值: null

4.2 命名路由傳值

所謂“命名路由”(Named Route)即有名字的路由,我們可以先給路由起一個名字,然後就可以通過路由名字直接開啟新的路由了,這為路由管理帶來了一種直觀、簡單的方式。

4.2.1 路由表

要想使用命名路由,我們必須先提供並註冊一個路由表(routing table),這樣應用程式才知道哪個名字與哪個路由元件相對應。其實註冊路由表就是給路由起名字,路由表的定義如下:

dart Map<String, WidgetBuilder> routes;

它是一個Map,key為路由的名字,是個字串;value是個builder回撥函式,用於生成相應的路由widget。我們在通過路由名字開啟新路由時,應用會根據路由名字在路由表中查詢到對應的WidgetBuilder回撥函式,然後呼叫該回調函式生成路由widget並返回。

4.2.2 註冊路由表

路由表的註冊方式很簡單,我們回到之前“計數器”的示例,然後在MyApp類的build方法中找到MaterialApp,新增routes屬性,程式碼如下:

dart MaterialApp( title: 'Flutter Demo', theme: ThemeData( primarySwatch: Colors.blue, ), //註冊路由表 routes:{ "new_page":(context) => NewRoute(), ... // 省略其他路由註冊資訊 } , home: MyHomePage(title: 'Flutter Demo Home Page'), );

現在我們就完成了路由表的註冊。上面的程式碼中home路由並沒有使用命名路由,如果我們也想將home註冊為命名路由應該怎麼做呢?其實很簡單,直接看程式碼:

dart MaterialApp( title: 'Flutter Demo', initialRoute:"/", //名為"/"的路由作為應用的home(首頁) theme: ThemeData( primarySwatch: Colors.blue, ), //註冊路由表 routes:{ "new_page":(context) => NewRoute(), "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //註冊首頁路由 } );

可以看到,我們只需在路由表中註冊一下MyHomePage路由,然後將其名字作為MaterialAppinitialRoute屬性值即可,該屬性決定應用的初始路由頁是哪一個命名路由。

4.2.3 通過路由名開啟新路由頁

要通過路由名稱來開啟新路由,可以使用NavigatorpushNamed方法:

dart Future pushNamed(BuildContext context, String routeName,{Object arguments})

Navigator 除了pushNamed方法,還有pushReplacementNamed等其他管理命名路由的方法,讀者可以自行檢視API文件。接下來我們通過路由名來開啟新的路由頁,修改TextButtononPressed回撥程式碼,改為:

dart onPressed: () { Navigator.pushNamed(context, "new_page"); //Navigator.push(context, // MaterialPageRoute(builder: (context) { // return NewRoute(); //})); },

熱過載應用,再次點選“open new route”按鈕,依然可以開啟新的路由頁。

4.2.4 命名路由引數傳遞

在Flutter最初的版本中,命名路由是不能傳遞引數的,後來才支援了引數;下面展示命名路由如何傳遞並獲取路由引數:

我們先註冊一個路由:

dart routes:{ "new_page":(context) => EchoRoute(), } ,

在路由頁通過RouteSetting物件獲取路由引數:

```dart class EchoRoute extends StatelessWidget {

@override Widget build(BuildContext context) { //獲取路由引數
var args=ModalRoute.of(context).settings.arguments; //...省略無關程式碼 } } ```

在開啟路由時傳遞引數

dart Navigator.of(context).pushNamed("new_page", arguments: "hi");

4.2.5 適配

假設我們也想將上面路由傳參示例中的TipRoute路由頁註冊到路由表中,以便也可以通過路由名來開啟它。但是,由於TipRoute接受一個text 引數,我們如何在不改變TipRoute原始碼的前提下適配這種情況?其實很簡單:

dart MaterialApp( ... //省略無關程式碼 routes: { "tip2": (context){ return TipRoute(text: ModalRoute.of(context)!.settings.arguments); }, }, );

4.3 路由生成鉤子

4.3.1 onGenerateRoute

假設我們要開發一個電商App,當用戶沒有登入時可以看店鋪、商品等資訊,但交易記錄、購物車、使用者個人資訊等頁面需要登入後才能看。為了實現上述功能,我們需要在開啟每一個路由頁前判斷使用者登入狀態!如果每次開啟路由前我們都需要去判斷一下將會非常麻煩,那有什麼更好的辦法嗎?答案是有!

MaterialApp有一個onGenerateRoute屬性,它在開啟命名路由時可能會被呼叫,之所以說可能,是因為當呼叫Navigator.pushNamed(...)開啟命名路由時,如果指定的路由名在路由表中已註冊,則會呼叫路由表中的builder函式來生成路由元件;如果路由表中沒有註冊,才會呼叫onGenerateRoute來生成路由。onGenerateRoute回撥簽名如下:

dart Route<dynamic> Function(RouteSettings settings)

有了onGenerateRoute回撥,要實現上面控制頁面許可權的功能就非常容易:我們放棄使用路由表,取而代之的是提供一個onGenerateRoute回撥,然後在該回調中進行統一的許可權控制,如:

dart MaterialApp( ... //省略無關程式碼 onGenerateRoute:(RouteSettings settings){ return MaterialPageRoute(builder: (context){ String routeName = settings.name; // 如果訪問的路由頁需要登入,但當前未登入,則直接返回登入頁路由, // 引導使用者登入;其他情況則正常開啟路由。 } ); } );

注意,onGenerateRoute 只會對命名路由生效。

4.3.2 navigatorObservers(待補充)

監聽所有路由跳轉動作

4.3.3 onUnknownRoute(待補充)

開啟一個不存在的命名路由時會被呼叫

4.4 如何選擇路由管理方式

最好統一使用命名路由的管理方式,這將會帶來如下好處:

  1. 語義化更明確。
  2. 程式碼更好維護;如果使用匿名路由,則必須在呼叫Navigator.push的地方建立新路由頁,這樣不僅需要import新路由頁的dart檔案,而且這樣的程式碼將會非常分散。
  3. 可以通過onGenerateRoute做一些全域性的路由跳轉前置處理邏輯。

  4. 本文作者: Smallfan

  5. 本文出自:  程式猿小風扇(https://smallfan.net)
  6. 版權宣告: 本部落格所有文章除特別宣告外,均採用 BY-NC-SA 許可協議。轉載請註明出處!