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

2021-03-06 16:10 更新

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

#2.2.1 一個簡單示例

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

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

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

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

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

   Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
         ... //省略無關(guān)代碼
         FlatButton(
            child: Text("open new route"),
            textColor: Colors.blue,
            onPressed: () {
             //導(dǎo)航到新路由   
             Navigator.push( context,
              MaterialPageRoute(builder: (context) {
                 return NewRoute();
              }));
             },
            ),
          ],
    )

我們添加了一個打開新路由的按鈕,并將按鈕文字顏色設(shè)置為藍(lán)色,點擊該按鈕后就會打開新的路由頁面,效果如圖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)與平臺頁面切換動畫風(fēng)格一致的路由切換動畫:

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

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

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

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

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

#2.2.3 Navigator

Navigator是一個路由管理的組件,它提供了打開和退出路由頁方法。Navigator通過一個棧來管理活動路由集合。通常當(dāng)前屏幕顯示的頁面就是棧頂?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.replaceNavigator.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ù),負(fù)責(zé)將傳入它的文本顯示在頁面上,另外TipRoute中我們添加一個“返回”按鈕,點擊后在返回上一個路由的同時會帶上一個返回參數(shù),下面我們看一下實現(xiàn)代碼。

TipRoute實現(xiàn)代碼:

class TipRoute extends StatelessWidget {
  TipRoute({
    Key key,
    @required this.text,  // 接收一個text參數(shù)
  }) : 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: <Widget>[
              Text(text),
              RaisedButton(
                onPressed: () => Navigator.pop(context, "我是返回值"),
                child: Text("返回"),
              )
            ],
          ),
        ),
      ),
    );
  }
}

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

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

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

圖2-4

需要說明:

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

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

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

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

#2.2.5 命名路由

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

#路由表

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

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屬性,代碼如下:

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

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

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

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

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

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

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

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

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

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

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

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

我們先注冊一個路由:

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

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

class EchoRoute extends StatelessWidget {


  @override
  Widget build(BuildContext context) {
    //獲取路由參數(shù)  
    var args=ModalRoute.of(context).settings.arguments;
    //...省略無關(guān)代碼
  }
}

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

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

#適配

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

MaterialApp(
  ... //省略無關(guān)代碼
  routes: {
   "tip2": (context){
     return TipRoute(text: ModalRoute.of(context).settings.arguments);
   },
 }, 
);

#2.2.6 路由生成鉤子

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

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

Route<dynamic> Function(RouteSettings settings)

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

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

注意,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)前置處理邏輯。

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

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

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號