路由管理
FLutter中的路由倾鲫,和原生組件化的路由一樣粗合,就是頁(yè)面之間的跳轉(zhuǎn),也可以稱之為導(dǎo)航乌昔。app維護(hù)一個(gè)路由棧隙疚,路由入棧(push)操作對(duì)應(yīng)打開一個(gè)新頁(yè)面,路由出棧(pop)操作對(duì)應(yīng)頁(yè)面關(guān)閉操作磕道,而路由管理主要是指如何來管理路由棧供屉。
MaterialPageRoute
MaterialPageRoute是一種模態(tài)路由,可以針對(duì)不同平臺(tái)自適應(yīng)的過渡動(dòng)畫替換整個(gè)屏幕頁(yè)面:
對(duì)于Android溺蕉,打開新頁(yè)面時(shí)伶丐,新頁(yè)面從屏幕底部導(dǎo)入到頂部。關(guān)閉頁(yè)面的時(shí)候疯特,會(huì)從頂部滑動(dòng)到底部消失哗魂。
在iOS上,頁(yè)面從右側(cè)滑入并反向退出漓雅。
下面我們介紹一下MaterialPageRoute 構(gòu)造函數(shù)的各個(gè)參數(shù)的意義:
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
builder 是一個(gè)WidgetBuilder類型的回調(diào)函數(shù)录别,它的作用是構(gòu)建路由頁(yè)面的具體內(nèi)容朽色,返回值是一個(gè)widget。我們通常要實(shí)現(xiàn)此回調(diào)组题,返回新路由的實(shí)例葫男。
settings 包含路由的配置信息,如路由名稱往踢、是否初始路由(首頁(yè))腾誉。
maintainState:默認(rèn)情況下,當(dāng)入棧一個(gè)新路由時(shí)峻呕,原來的路由仍然會(huì)被保存在內(nèi)存中利职,如果想在路由沒用的時(shí)候釋放其所占用的所有資源,可以設(shè)置maintainState為false瘦癌。
fullscreenDialog表示新的路由頁(yè)面是否是一個(gè)全屏的模態(tài)對(duì)話框猪贪,在iOS中,如果fullscreenDialog為true讯私,新頁(yè)面將會(huì)從屏幕底部滑入(而不是水平方向)
基本使用
Flutter為我們提供了導(dǎo)航器Navigator热押。參數(shù)傳入當(dāng)前的BuildContext和要導(dǎo)航的頁(yè)面即可。
- 調(diào)用Navigator.push導(dǎo)航到第二個(gè)頁(yè)面
Navigator.push( context, new MaterialPageRoute(builder: (context) => Page2()));
- 調(diào)用Navigator.pop返回前一個(gè)頁(yè)面
Navigator.pop(context, result);
-
關(guān)閉頁(yè)面后獲取結(jié)果
有時(shí)候我們需要上個(gè)頁(yè)面關(guān)閉時(shí)傳遞一個(gè)返回值斤寇,幸運(yùn)的是桶癣,Navigator的調(diào)用方法都是Future,因此我們可以等待它們的結(jié)果:3.1. 等待Navigator運(yùn)行
3.2. 將返回值傳遞給Navigator.pop函數(shù)
3.3. 等待完成后娘锁,獲取返回值在page1中牙寞,導(dǎo)航到page2,并且await到page2傳遞返回值并pop莫秆,根據(jù)返回值彈出不同的對(duì)話框:
onPressed: () async { var navigationResult = await Navigator.push( context, new MaterialPageRoute(builder: (context) => Page2())); if (navigationResult == 'from_back') { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Navigation from back'), )); } else if (navigationResult == 'from_button') { showDialog( context: context, builder: (context) => AlertDialog( title: Text('Navigation from button'), )); } },
在page2中傳遞返回值并返回:
Navigator.pop(context, 'from_button');
攔截返回鍵
如果不想點(diǎn)擊返回鍵關(guān)閉當(dāng)前頁(yè)面间雀,可以使用WillPopScope小部件,用它放在最外層包括住腳手架镊屎。并向onWillPop返回false惹挟。false告訴系統(tǒng)當(dāng)前頁(yè)面不處理返回。
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: () => Future.value(false),
child: Scaffold(
body: Container(
child: Center(
child: Text('Page 2',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.bold)),
),
),
),
);
}
如果想自定義處理返回鍵缝驳,可以在return false 之前自己處理连锯,比如關(guān)閉 當(dāng)前頁(yè)面并傳遞返回值:
WillPopScope(
onWillPop: () async {
// You can await in the calling widget for my_value and handle when complete.
Navigator.pop(context, 'my_value');
return false;
},
...
);
命名路由
基本使用
上面代碼是在沒個(gè)需要導(dǎo)航的地方聲明路由,不能復(fù)用用狱,我們可以先給路由起一個(gè)名字萎庭,再注冊(cè)路由表,然后就可以通過路由名字直接打開新的路由了齿拂,這為路由管理帶來了一種直觀、簡(jiǎn)單的方式肴敛,并且可以復(fù)用署海。
MaterialApp的routes屬性吗购,既是注冊(cè)路由表用的,它對(duì)應(yīng)一個(gè)Map<String, WidgetBuilder>砸狞。
起名:
static const String page1 = "/page1";
static const String page2 = "/page2";
聲明路由表:
Map<String, WidgetBuilder> routes = {
page1: (context) => Page1(),
page2: (context) => Page2(),
};
注冊(cè)路由表:
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
routes: routes,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Page1(),
);
}
然后在需要路由的地方使用命名路由調(diào)用:
Navigator.pushNamed(context, page2)
傳遞參數(shù)
給page3起名:
page3: (context) => Page3(),
打開路由時(shí)候傳遞參數(shù):
Navigator.of(context).pushNamed(page3, arguments: "hi");
page3中接收參數(shù):
class Page3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
//獲取路由參數(shù)
var args = ModalRoute.of(context).settings.arguments;
return Scaffold(
body: Container(
child: Center(
child: Text('Page 3的參數(shù)是$args',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.bold)),
),
),
);
}
}
構(gòu)造函數(shù)傳參
上面我們明明給page3傳遞了參數(shù)捻勉,但是并非傳遞到構(gòu)造函數(shù)上。我們看構(gòu)造函數(shù)刀森,并不知道傳遞了什么參數(shù)踱启,必須去看路由,并不是很好的做法研底。那怎么給構(gòu)造函數(shù)傳參呢埠偿?
起名:
const String page4 = "/page4";
注冊(cè)路由:
page4: (context) => Page4(text: ModalRoute.of(context).settings.arguments),
打開路由時(shí)傳遞參數(shù):
Navigator.of(context).pushNamed(page4, arguments: "hello");
動(dòng)態(tài)路由
MaterialApp還為我們提供了一個(gè)onGenerateRoute參數(shù),未在路由表里注冊(cè)的路由榜晦,會(huì)在這里尋找冠蒋。RouteFactory有一個(gè)RouteSettings參數(shù),并返回一個(gè)Route<dynamic>乾胶。這是我們將用來執(zhí)行所有路由的功能抖剿。
Route<dynamic> Function(RouteSettings settings)
我們可以這樣使用:
先聲明路由表:
Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case page5:
return MaterialPageRoute(builder: (context) => Page5());
case page6:
return MaterialPageRoute(builder: (context) => Page6());
default:
return MaterialPageRoute(builder: (context) => Page1());
}
注冊(cè):
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
routes: routes,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
onGenerateRoute: generateRoute,
home: Page1(),
);
}
打開路由:
Navigator.of(context).pushNamed(page5);
動(dòng)態(tài)路由傳遞參數(shù)
上面說了,settings可以拿到參數(shù)识窿,我們當(dāng)然就可以傳遞參數(shù)了:
Route<dynamic> generateRoute(RouteSettings settings) {
print('====${settings.name}');
switch (settings.name) {
case page5:
return MaterialPageRoute(builder: (context) => Page5());
case page6:
return MaterialPageRoute(builder: (context) => Page6(text: settings.arguments,));
default:
return MaterialPageRoute(builder: (context) => Page1());
}
}
使用:
Navigator.of(context).pushNamed(page6, arguments: "world");
so easy斩郎。
處理未定義的路線
有兩種處理未定義路由的方法。
- 利用generateRoute喻频,找不到路由名的返回默認(rèn)路由
Route<dynamic> generateRoute(RouteSettings settings) {
print('====${settings.name}');
switch (settings.name) {
case page5:
return MaterialPageRoute(builder: (context) => Page5());
case page6:
return MaterialPageRoute(builder: (context) => Page6(text: settings.arguments,));
default:
return MaterialPageRoute(builder: (context) => NotFindPage());
}
}
- 利用onUnknownRoute返回默認(rèn)路由
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
routes: routes,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
onGenerateRoute: generateRoute,
onUnknownRoute: (settings) =>
MaterialPageRoute(builder: (context) => NotFindPage()));
}
初始路由
打開應(yīng)用第一屏的路由缩宜,也有2種方式,
- 可以設(shè)置initialRoute半抱,指定路由表里注冊(cè)的路由名脓恕。
- 可以設(shè)置home,對(duì)應(yīng)的page窿侈。
initialRoute會(huì)覆蓋home炼幔。
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
routes: routes,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
initialRoute: root,
home: Page2(),
onGenerateRoute: generateRoute,
onUnknownRoute: (settings) =>
MaterialPageRoute(builder: (context) => NotFindPage()));
}
不使用BuildContext的路由導(dǎo)航
很多情況是,我們已將UI代碼從業(yè)務(wù)邏輯中分離出來(類似于MVVM架構(gòu))史简。viewModel應(yīng)處理所有邏輯乃秀,視圖應(yīng)僅調(diào)用模型上的函數(shù),然后在需要時(shí)使用新狀態(tài)重建自身圆兵。
我們知道Navigator需要BuildContext的參數(shù)跺讯,我們?cè)谶M(jìn)行實(shí)際業(yè)務(wù)邏輯決策的位置進(jìn)行導(dǎo)航,而不是在widget里調(diào)用路由,如果在viewModel里導(dǎo)航殉农,就要傳入context嗎刀脏?下面實(shí)現(xiàn)不要context的導(dǎo)航。
為了遵守MVVM原則超凳,我們將把Navigation功能移動(dòng)到可以從viewModel調(diào)用的服務(wù)中愈污。在lib下創(chuàng)建一個(gè)名為services的新文件夾耀态,并在其中創(chuàng)建一個(gè)名為navigation_service.dart的新文件。
先實(shí)現(xiàn)單利模式:
class NavigationService {
factory NavigationService.getInstance() => _getInstance();
NavigationService._internal();
static NavigationService _instance;
static NavigationService _getInstance() {
if (_instance == null) {
_instance = new NavigationService._internal();
}
return _instance;
}
}
然后利用navigatorKey實(shí)現(xiàn):
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState.pushNamed(routeName);
}
void goBack() {
return navigatorKey.currentState.pop();
}
我們將NavigationService與應(yīng)用程序鏈接的方式暂雹,通過navigatorKey提供給MaterialApp首装。轉(zhuǎn)到main.dart文件并設(shè)置navigatorKey:
MaterialApp(
title: 'Flutter Demo',
navigatorKey: NavigationService().navigatorKey,
...
)
然后寫一個(gè)viewModel,嘗試導(dǎo)航:
class ViewModel {
final NavigationService _navigationService = NavigationService();
Future goPage1() async{
/// 模擬請(qǐng)求數(shù)據(jù)后調(diào)到首頁(yè)
await Future.delayed(Duration(seconds: 1));
_navigationService.navigateTo(page1);
}
}
在page6里使用viewModel導(dǎo)航:
onPressed: () {
viewModel.goPage1();
},
現(xiàn)在杭跪,將View文件的職責(zé)帶回到了“顯示UI”并將用戶操作傳遞給模型仙逻,而不是“顯示UI”將用戶操作傳遞給模型并進(jìn)行導(dǎo)航。更符合MVVM職責(zé)的劃分涧尿。
這樣做的好處是系奉,隨著導(dǎo)航邏輯的擴(kuò)展,我們的UI將保持不變现斋,并且模型將承載所有邏輯/狀態(tài)管理喜最。