路由(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)閉操作,而路由管理主要是指如何來管理路由棧。
我們在上一節(jié)“計數(shù)器”示例的基礎(chǔ)上,做如下修改:
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"。
_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所示。
MaterialPageRoute
繼承自PageRoute
類,PageRoute
類是一個抽象類,表示占有整個屏幕空間的一個模態(tài)路由頁面,它還定義了路由構(gòu)建及切換時過渡動畫的相關(guān)接口及屬性。MaterialPageRoute
是 Material 組件庫提供的組件,它可以針對不同平臺,實現(xiàn)與平臺頁面切換動畫風(fēng)格一致的路由切換動畫:
下面我們介紹一下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 中,如果fullscreenDialog
為true
,新頁面將會從屏幕底部滑入(而不是水平方向)。如果想自定義路由切換動畫,可以自己繼承 PageRoute 來實現(xiàn),我們將在后面介紹動畫時,實現(xiàn)一個自定義的路由組件。
Navigator
是一個路由管理的組件,它提供了打開和退出路由頁方法。Navigator
通過一個棧來管理活動路由集合。通常當(dāng)前屏幕顯示的頁面就是棧頂?shù)穆酚伞?code>Navigator提供了一系列方法來管理路由棧,在此我們只介紹其最常用的兩個方法:
將給定的路由入棧(即打開新的頁面),返回值是一個Future
對象,用以接收新路由出棧(即關(guān)閉)時的返回數(shù)據(jù)。
將棧頂路由出棧,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)的方法也是一樣的。
很多時候,在路由跳轉(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所示下:
需要說明:
TipRoute
的text
參數(shù)傳遞給新路由頁的。我們可以通過等待Navigator.push(…)
返回的Future
來獲取新路由的返回數(shù)據(jù)。TipRoute
頁中有兩種方式可以返回到上一頁;第一種方式時直接點擊導(dǎo)航欄返回箭頭,第二種方式是點擊頁面中的“返回”按鈕。這兩種返回方式的區(qū)別是前者不會返回數(shù)據(jù)給上一個路由,而后者會。下面是分別點擊頁面中的返回按鈕和導(dǎo)航欄返回箭頭后,RouterTestRoute
頁中print
方法在控制臺輸出的內(nèi)容: I/flutter (27896): 路由返回值: 我是返回值
I/flutter (27896): 路由返回值: null
上面介紹的是非命名路由的傳值方式,命名路由的傳值方式會有所不同,我們會在下面介紹命名路由時介紹。
所謂“命名路由”(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
路由,然后將其名字作為MaterialApp
的initialRoute
屬性值即可,該屬性決定應(yīng)用的初始路由頁是哪一個命名路由。
要通過路由名稱來打開新路由,可以使用Navigator
的pushNamed
方法:
Future pushNamed(BuildContext context, String routeName,{Object arguments})
Navigator
除了pushNamed
方法,還有pushReplacementNamed
等其他管理命名路由的方法,讀者可以自行查看 API 文檔。接下來我們通過路由名來打開新的路由頁,修改FlatButton
的onPressed
回調(diào)代碼,改為:
onPressed: () {
Navigator.pushNamed(context, "new_page");
//Navigator.push(context,
// MaterialPageRoute(builder: (context) {
// return NewRoute();
//}));
},
熱重載應(yīng)用,再次點擊“open new route”按鈕,依然可以打開新的路由頁。
在 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);
},
},
);
假設(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
只會對命名路由生效。
本章先介紹了 Flutter 中路由管理、傳參的方式,然后又著重介紹了命名路由相關(guān)內(nèi)容。在此需要說明一點,由于命名路由只是一種可選的路由管理方式,在實際開發(fā)中,讀者可能心中會猶豫到底使用哪種路由管理方式。在此,根據(jù)筆者經(jīng)驗,建議讀者最好統(tǒng)一使用命名路由的管理方式,這將會帶來如下好處:
Navigator.push
的地方創(chuàng)建新路由頁,這樣不僅需要 import 新路由頁的 dart 文件,而且這樣的代碼將會非常分散。onGenerateRoute
做一些全局的路由跳轉(zhuǎn)前置處理邏輯。綜上所述,筆者比較建議使用命名路由,當(dāng)然這并不是什么金科玉律,讀者可以根據(jù)自己偏好或?qū)嶋H情況來決定。
另外,還有一些關(guān)于路由管理的內(nèi)容我們沒有介紹,比如路由 MaterialApp 中還有navigatorObservers
和onUnknownRoute
兩個回調(diào)屬性,前者可以監(jiān)聽所有路由跳轉(zhuǎn)動作,后者在打開一個不存在的命名路由時會被調(diào)用,由于這些功能并不常用,而且也比較簡單,我們便不再花費篇幅來介紹了,讀者可以自行查看 API 文檔。
更多建議: