Flutter實戰(zhàn) 路由管理

2021-03-06 16:10 更新

路由(Route)在移動開發(fā)中通常指頁面(Page),這跟 web 開發(fā)中單頁應(yīng)用的 Route 概念意義是相同的,Route 在 Android 中通常指一個 Activity,在 iOS 中指一個ViewController。所謂路由管理,就是管理頁面之間如何跳轉(zhuǎn),通常也可被稱為導航管理。Flutter 中的路由管理和原生開發(fā)類似,無論是 Android 還是 iOS,導航管理都會維護一個路由棧,路由入棧(push)操作對應(yīng)打開一個新頁面,路由出棧(pop)操作對應(yīng)頁面關(guān)閉操作,而路由管理主要是指如何來管理路由棧。

#2.2.1 一個簡單示例

我們在上一節(jié)“計數(shù)器”示例的基礎(chǔ)上,做如下修改:

  1. 創(chuàng)建一個新路由,命名“NewRoute”

  1. class NewRoute extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Scaffold(
  5. appBar: AppBar(
  6. title: Text("New route"),
  7. ),
  8. body: Center(
  9. child: Text("This is new route"),
  10. ),
  11. );
  12. }
  13. }

新路由繼承自StatelessWidget,界面很簡單,在頁面中間顯示一句"This is new route"。

  1. _MyHomePageState.build方法中的Column的子 widget 中添加一個按鈕(FlatButton) :

  1. Column(
  2. mainAxisAlignment: MainAxisAlignment.center,
  3. children: <Widget>[
  4. ... //省略無關(guān)代碼
  5. FlatButton(
  6. child: Text("open new route"),
  7. textColor: Colors.blue,
  8. onPressed: () {
  9. //導航到新路由
  10. Navigator.push( context,
  11. MaterialPageRoute(builder: (context) {
  12. return NewRoute();
  13. }));
  14. },
  15. ),
  16. ],
  17. )

我們添加了一個打開新路由的按鈕,并將按鈕文字顏色設(shè)置為藍色,點擊該按鈕后就會打開新的路由頁面,效果如圖2-2和2-3所示。

圖2-2 圖2-3

#2.2.2 MaterialPageRoute

MaterialPageRoute繼承自PageRoute類,PageRoute類是一個抽象類,表示占有整個屏幕空間的一個模態(tài)路由頁面,它還定義了路由構(gòu)建及切換時過渡動畫的相關(guān)接口及屬性。MaterialPageRoute 是 Material 組件庫提供的組件,它可以針對不同平臺,實現(xiàn)與平臺頁面切換動畫風格一致的路由切換動畫:

  • 對于 Android,當打開新頁面時,新的頁面會從屏幕底部滑動到屏幕頂部;當關(guān)閉頁面時,當前頁面會從屏幕頂部滑動到屏幕底部后消失,同時上一個頁面會顯示到屏幕上。
  • 對于 iOS,當打開頁面時,新的頁面會從屏幕右側(cè)邊緣一致滑動到屏幕左邊,直到新頁面全部顯示到屏幕上,而上一個頁面則會從當前屏幕滑動到屏幕左側(cè)而消失;當關(guān)閉頁面時,正好相反,當前頁面會從屏幕右側(cè)滑出,同時上一個頁面會從屏幕左側(cè)滑入。

下面我們介紹一下MaterialPageRoute 構(gòu)造函數(shù)的各個參數(shù)的意義:

  1. MaterialPageRoute({
  2. WidgetBuilder builder,
  3. RouteSettings settings,
  4. bool maintainState = true,
  5. bool fullscreenDialog = false,
  6. })

  • builder 是一個 WidgetBuilder 類型的回調(diào)函數(shù),它的作用是構(gòu)建路由頁面的具體內(nèi)容,返回值是一個widget。我們通常要實現(xiàn)此回調(diào),返回新路由的實例。
  • settings 包含路由的配置信息,如路由名稱、是否初始路由(首頁)。
  • maintainState:默認情況下,當入棧一個新路由時,原來的路由仍然會被保存在內(nèi)存中,如果想在路由沒用的時候釋放其所占用的所有資源,可以設(shè)置maintainState為false。
  • fullscreenDialog表示新的路由頁面是否是一個全屏的模態(tài)對話框,在 iOS 中,如果fullscreenDialogtrue,新頁面將會從屏幕底部滑入(而不是水平方向)。

如果想自定義路由切換動畫,可以自己繼承 PageRoute 來實現(xiàn),我們將在后面介紹動畫時,實現(xiàn)一個自定義的路由組件。

#2.2.3 Navigator

Navigator是一個路由管理的組件,它提供了打開和退出路由頁方法。Navigator通過一個棧來管理活動路由集合。通常當前屏幕顯示的頁面就是棧頂?shù)穆酚伞?code>Navigator提供了一系列方法來管理路由棧,在此我們只介紹其最常用的兩個方法:

#Future push(BuildContext context, Route route)

將給定的路由入棧(即打開新的頁面),返回值是一個Future對象,用以接收新路由出棧(即關(guān)閉)時的返回數(shù)據(jù)。

#bool pop(BuildContext context, [ result ])

將棧頂路由出棧,result為頁面關(guān)閉時返回給上一個頁面的數(shù)據(jù)。

Navigator 還有很多其它方法,如Navigator.replace、Navigator.popUntil等,詳情請參考 API 文檔或 SDK 源碼注釋,在此不再贅述。下面我們還需要介紹一下路由相關(guān)的另一個概念“命名路由”。

#實例方法

Navigator 類中第一個參數(shù)為 context 的靜態(tài)方法都對應(yīng)一個 Navigator 的實例方法, 比如Navigator.push(BuildContext context, Route route)等價于Navigator.of(context).push(Route route) ,下面命名路由相關(guān)的方法也是一樣的。

#2.2.4 路由傳值

很多時候,在路由跳轉(zhuǎn)時我們需要帶一些參數(shù),比如打開商品詳情頁時,我們需要帶一個商品id,這樣商品詳情頁才知道展示哪個商品信息;又比如我們在填寫訂單時需要選擇收貨地址,打開地址選擇頁并選擇地址后,可以將用戶選擇的地址返回到訂單頁等等。下面我們通過一個簡單的示例來演示新舊路由如何傳參。

#示例

我們創(chuàng)建一個TipRoute路由,它接受一個提示文本參數(shù),負責將傳入它的文本顯示在頁面上,另外TipRoute中我們添加一個“返回”按鈕,點擊后在返回上一個路由的同時會帶上一個返回參數(shù),下面我們看一下實現(xiàn)代碼。

TipRoute實現(xiàn)代碼:

  1. class TipRoute extends StatelessWidget {
  2. TipRoute({
  3. Key key,
  4. @required this.text, // 接收一個text參數(shù)
  5. }) : super(key: key);
  6. final String text;
  7. @override
  8. Widget build(BuildContext context) {
  9. return Scaffold(
  10. appBar: AppBar(
  11. title: Text("提示"),
  12. ),
  13. body: Padding(
  14. padding: EdgeInsets.all(18),
  15. child: Center(
  16. child: Column(
  17. children: <Widget>[
  18. Text(text),
  19. RaisedButton(
  20. onPressed: () => Navigator.pop(context, "我是返回值"),
  21. child: Text("返回"),
  22. )
  23. ],
  24. ),
  25. ),
  26. ),
  27. );
  28. }
  29. }

下面是打開新路由TipRoute的代碼:

  1. class RouterTestRoute extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. return Center(
  5. child: RaisedButton(
  6. onPressed: () async {
  7. // 打開`TipRoute`,并等待返回結(jié)果
  8. var result = await Navigator.push(
  9. context,
  10. MaterialPageRoute(
  11. builder: (context) {
  12. return TipRoute(
  13. // 路由參數(shù)
  14. text: "我是提示xxxx",
  15. );
  16. },
  17. ),
  18. );
  19. //輸出`TipRoute`路由返回結(jié)果
  20. print("路由返回值: $result");
  21. },
  22. child: Text("打開提示頁"),
  23. ),
  24. );
  25. }
  26. }

運行上面代碼,點擊RouterTestRoute頁的“打開提示頁”按鈕,會打開TipRoute頁,運行效果如圖2-4所示下:

圖2-4

需要說明:

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

  1. TipRoute頁中有兩種方式可以返回到上一頁;第一種方式時直接點擊導航欄返回箭頭,第二種方式是點擊頁面中的“返回”按鈕。這兩種返回方式的區(qū)別是前者不會返回數(shù)據(jù)給上一個路由,而后者會。下面是分別點擊頁面中的返回按鈕和導航欄返回箭頭后,RouterTestRoute頁中print方法在控制臺輸出的內(nèi)容:

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

上面介紹的是非命名路由的傳值方式,命名路由的傳值方式會有所不同,我們會在下面介紹命名路由時介紹。

#2.2.5 命名路由

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

#路由表

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

  1. Map<String, WidgetBuilder> routes;

它是一個Map,key 為路由的名字,是個字符串;value 是個builder回調(diào)函數(shù),用于生成相應(yīng)的路由 widget。我們在通過路由名字打開新路由時,應(yīng)用會根據(jù)路由名字在路由表中查找到對應(yīng)的WidgetBuilder回調(diào)函數(shù),然后調(diào)用該回調(diào)函數(shù)生成路由 widget 并返回。

#注冊路由表

路由表的注冊方式很簡單,我們回到之前“計數(shù)器”的示例,然后在MyApp類的build方法中找到MaterialApp,添加routes屬性,代碼如下:

  1. MaterialApp(
  2. title: 'Flutter Demo',
  3. theme: ThemeData(
  4. primarySwatch: Colors.blue,
  5. ),
  6. //注冊路由表
  7. routes:{
  8. "new_page":(context) => NewRoute(),
  9. ... // 省略其它路由注冊信息
  10. } ,
  11. home: MyHomePage(title: 'Flutter Demo Home Page'),
  12. );

現(xiàn)在我們就完成了路由表的注冊。上面的代碼中home路由并沒有使用命名路由,如果我們也想將home注冊為命名路由應(yīng)該怎么做呢?其實很簡單,直接看代碼:

  1. MaterialApp(
  2. title: 'Flutter Demo',
  3. initialRoute:"/", //名為"/"的路由作為應(yīng)用的home(首頁)
  4. theme: ThemeData(
  5. primarySwatch: Colors.blue,
  6. ),
  7. //注冊路由表
  8. routes:{
  9. "new_page":(context) => NewRoute(),
  10. "/":(context) => MyHomePage(title: 'Flutter Demo Home Page'), //注冊首頁路由
  11. }
  12. );

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

#通過路由名打開新路由頁

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

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

Navigator 除了pushNamed方法,還有pushReplacementNamed等其他管理命名路由的方法,讀者可以自行查看 API 文檔。接下來我們通過路由名來打開新的路由頁,修改FlatButtononPressed回調(diào)代碼,改為:

  1. onPressed: () {
  2. Navigator.pushNamed(context, "new_page");
  3. //Navigator.push(context,
  4. // MaterialPageRoute(builder: (context) {
  5. // return NewRoute();
  6. //}));
  7. },

熱重載應(yīng)用,再次點擊“open new route”按鈕,依然可以打開新的路由頁。

#命名路由參數(shù)傳遞

在 Flutter 最初的版本中,命名路由是不能傳遞參數(shù)的,后來才支持了參數(shù);下面展示命名路由如何傳遞并獲取路由參數(shù):

我們先注冊一個路由:

  1. routes:{
  2. "new_page":(context) => EchoRoute(),
  3. } ,

在路由頁通過RouteSetting對象獲取路由參數(shù):

  1. class EchoRoute extends StatelessWidget {
  2. @override
  3. Widget build(BuildContext context) {
  4. //獲取路由參數(shù)
  5. var args=ModalRoute.of(context).settings.arguments;
  6. //...省略無關(guān)代碼
  7. }
  8. }

在打開路由時傳遞參數(shù)

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

#適配

假設(shè)我們也想將上面路由傳參示例中的TipRoute路由頁注冊到路由表中,以便也可以通過路由名來打開它。但是,由于TipRoute接受一個text 參數(shù),我們?nèi)绾卧诓桓淖?code>TipRoute源碼的前提下適配這種情況?其實很簡單:

  1. MaterialApp(
  2. ... //省略無關(guān)代碼
  3. routes: {
  4. "tip2": (context){
  5. return TipRoute(text: ModalRoute.of(context).settings.arguments);
  6. },
  7. },
  8. );

#2.2.6 路由生成鉤子

假設(shè)我們要開發(fā)一個電商 APP,當用戶沒有登錄時可以看店鋪、商品等信息,但交易記錄、購物車、用戶個人信息等頁面需要登錄后才能看。為了實現(xiàn)上述功能,我們需要在打開每一個路由頁前判斷用戶登錄狀態(tài)!如果每次打開路由前我們都需要去判斷一下將會非常麻煩,那有什么更好的辦法嗎?答案是有!

MaterialApp有一個onGenerateRoute屬性,它在打開命名路由時可能會被調(diào)用,之所以說可能,是因為當調(diào)用Navigator.pushNamed(...)打開命名路由時,如果指定的路由名在路由表中已注冊,則會調(diào)用路由表中的builder函數(shù)來生成路由組件;如果路由表中沒有注冊,才會調(diào)用onGenerateRoute來生成路由。onGenerateRoute回調(diào)簽名如下:

  1. Route<dynamic> Function(RouteSettings settings)

有了onGenerateRoute回調(diào),要實現(xiàn)上面控制頁面權(quán)限的功能就非常容易:我們放棄使用路由表,取而代之的是提供一個onGenerateRoute回調(diào),然后在該回調(diào)中進行統(tǒng)一的權(quán)限控制,如:

  1. MaterialApp(
  2. ... //省略無關(guān)代碼
  3. onGenerateRoute:(RouteSettings settings){
  4. return MaterialPageRoute(builder: (context){
  5. String routeName = settings.name;
  6. // 如果訪問的路由頁需要登錄,但當前未登錄,則直接返回登錄頁路由,
  7. // 引導用戶登錄;其它情況則正常打開路由。
  8. }
  9. );
  10. }
  11. );

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

#2.2.7 總結(jié)

本章先介紹了 Flutter 中路由管理、傳參的方式,然后又著重介紹了命名路由相關(guān)內(nèi)容。在此需要說明一點,由于命名路由只是一種可選的路由管理方式,在實際開發(fā)中,讀者可能心中會猶豫到底使用哪種路由管理方式。在此,根據(jù)筆者經(jīng)驗,建議讀者最好統(tǒng)一使用命名路由的管理方式,這將會帶來如下好處:

  1. 語義化更明確。
  2. 代碼更好維護;如果使用匿名路由,則必須在調(diào)用Navigator.push的地方創(chuàng)建新路由頁,這樣不僅需要 import 新路由頁的 dart 文件,而且這樣的代碼將會非常分散。
  3. 可以通過onGenerateRoute做一些全局的路由跳轉(zhuǎn)前置處理邏輯。

綜上所述,筆者比較建議使用命名路由,當然這并不是什么金科玉律,讀者可以根據(jù)自己偏好或?qū)嶋H情況來決定。

另外,還有一些關(guān)于路由管理的內(nèi)容我們沒有介紹,比如路由 MaterialApp 中還有navigatorObserversonUnknownRoute兩個回調(diào)屬性,前者可以監(jiān)聽所有路由跳轉(zhuǎn)動作,后者在打開一個不存在的命名路由時會被調(diào)用,由于這些功能并不常用,而且也比較簡單,我們便不再花費篇幅來介紹了,讀者可以自行查看 API 文檔。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號