路由管理
FLutter中的路由,和原生組件化的路由一樣,就是頁面之間的跳轉,也可以稱之為導航。app維護一個路由棧,路由入棧(push)操作對應打開一個新頁面,路由出棧(pop)操作對應頁面關閉操作,而路由管理主要是指如何來管理路由棧。
MaterialPageRoute
MaterialPageRoute是一種模態路由,可以針對不同平臺自適應的過渡動畫替換整個屏幕頁面:
對于Android,打開新頁面時,新頁面從屏幕底部導入到頂部。關閉頁面的時候,會從頂部滑動到底部消失。
在iOS上,頁面從右側滑入并反向退出。
下面我們介紹一下MaterialPageRoute 構造函數的各個參數的意義:
MaterialPageRoute({
WidgetBuilder builder,
RouteSettings settings,
bool maintainState = true,
bool fullscreenDialog = false,
})
builder 是一個WidgetBuilder類型的回調函數,它的作用是構建路由頁面的具體內容,返回值是一個widget。我們通常要實現此回調,返回新路由的實例。
settings 包含路由的配置信息,如路由名稱、是否初始路由(首頁)。
maintainState:默認情況下,當入棧一個新路由時,原來的路由仍然會被保存在內存中,如果想在路由沒用的時候釋放其所占用的所有資源,可以設置maintainState為false。
fullscreenDialog表示新的路由頁面是否是一個全屏的模態對話框,在iOS中,如果fullscreenDialog為true,新頁面將會從屏幕底部滑入(而不是水平方向)
基本使用
Flutter為我們提供了導航器Navigator。參數傳入當前的BuildContext和要導航的頁面即可。
- 調用Navigator.push導航到第二個頁面
Navigator.push( context, new MaterialPageRoute(builder: (context) => Page2()));
- 調用Navigator.pop返回前一個頁面
Navigator.pop(context, result);
-
關閉頁面后獲取結果
有時候我們需要上個頁面關閉時傳遞一個返回值,幸運的是,Navigator的調用方法都是Future,因此我們可以等待它們的結果:3.1. 等待Navigator運行
3.2. 將返回值傳遞給Navigator.pop函數
3.3. 等待完成后,獲取返回值在page1中,導航到page2,并且await到page2傳遞返回值并pop,根據返回值彈出不同的對話框:
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');
攔截返回鍵
如果不想點擊返回鍵關閉當前頁面,可以使用WillPopScope小部件,用它放在最外層包括住腳手架。并向onWillPop返回false。false告訴系統當前頁面不處理返回。
@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 之前自己處理,比如關閉 當前頁面并傳遞返回值:
WillPopScope(
onWillPop: () async {
// You can await in the calling widget for my_value and handle when complete.
Navigator.pop(context, 'my_value');
return false;
},
...
);
命名路由
基本使用
上面代碼是在沒個需要導航的地方聲明路由,不能復用,我們可以先給路由起一個名字,再注冊路由表,然后就可以通過路由名字直接打開新的路由了,這為路由管理帶來了一種直觀、簡單的方式,并且可以復用。
MaterialApp的routes屬性,既是注冊路由表用的,它對應一個Map<String, WidgetBuilder>。
起名:
static const String page1 = "/page1";
static const String page2 = "/page2";
聲明路由表:
Map<String, WidgetBuilder> routes = {
page1: (context) => Page1(),
page2: (context) => Page2(),
};
注冊路由表:
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
routes: routes,
theme: ThemeData(
primarySwatch: Colors.blue,
visualDensity: VisualDensity.adaptivePlatformDensity,
),
home: Page1(),
);
}
然后在需要路由的地方使用命名路由調用:
Navigator.pushNamed(context, page2)
傳遞參數
給page3起名:
page3: (context) => Page3(),
打開路由時候傳遞參數:
Navigator.of(context).pushNamed(page3, arguments: "hi");
page3中接收參數:
class Page3 extends StatelessWidget {
@override
Widget build(BuildContext context) {
//獲取路由參數
var args = ModalRoute.of(context).settings.arguments;
return Scaffold(
body: Container(
child: Center(
child: Text('Page 3的參數是$args',
style: TextStyle(fontSize: 30.0, fontWeight: FontWeight.bold)),
),
),
);
}
}
構造函數傳參
上面我們明明給page3傳遞了參數,但是并非傳遞到構造函數上。我們看構造函數,并不知道傳遞了什么參數,必須去看路由,并不是很好的做法。那怎么給構造函數傳參呢?
起名:
const String page4 = "/page4";
注冊路由:
page4: (context) => Page4(text: ModalRoute.of(context).settings.arguments),
打開路由時傳遞參數:
Navigator.of(context).pushNamed(page4, arguments: "hello");
動態路由
MaterialApp還為我們提供了一個onGenerateRoute參數,未在路由表里注冊的路由,會在這里尋找。RouteFactory有一個RouteSettings參數,并返回一個Route<dynamic>。這是我們將用來執行所有路由的功能。
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());
}
注冊:
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);
動態路由傳遞參數
上面說了,settings可以拿到參數,我們當然就可以傳遞參數了:
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,找不到路由名的返回默認路由
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返回默認路由
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()));
}
初始路由
打開應用第一屏的路由,也有2種方式,
- 可以設置initialRoute,指定路由表里注冊的路由名。
- 可以設置home,對應的page。
initialRoute會覆蓋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的路由導航
很多情況是,我們已將UI代碼從業務邏輯中分離出來(類似于MVVM架構)。viewModel應處理所有邏輯,視圖應僅調用模型上的函數,然后在需要時使用新狀態重建自身。
我們知道Navigator需要BuildContext的參數,我們在進行實際業務邏輯決策的位置進行導航,而不是在widget里調用路由,如果在viewModel里導航,就要傳入context嗎?下面實現不要context的導航。
為了遵守MVVM原則,我們將把Navigation功能移動到可以從viewModel調用的服務中。在lib下創建一個名為services的新文件夾,并在其中創建一個名為navigation_service.dart的新文件。
先實現單利模式:
class NavigationService {
factory NavigationService.getInstance() => _getInstance();
NavigationService._internal();
static NavigationService _instance;
static NavigationService _getInstance() {
if (_instance == null) {
_instance = new NavigationService._internal();
}
return _instance;
}
}
然后利用navigatorKey實現:
final GlobalKey<NavigatorState> navigatorKey =
new GlobalKey<NavigatorState>();
Future<dynamic> navigateTo(String routeName) {
return navigatorKey.currentState.pushNamed(routeName);
}
void goBack() {
return navigatorKey.currentState.pop();
}
我們將NavigationService與應用程序鏈接的方式,通過navigatorKey提供給MaterialApp。轉到main.dart文件并設置navigatorKey:
MaterialApp(
title: 'Flutter Demo',
navigatorKey: NavigationService().navigatorKey,
...
)
然后寫一個viewModel,嘗試導航:
class ViewModel {
final NavigationService _navigationService = NavigationService();
Future goPage1() async{
/// 模擬請求數據后調到首頁
await Future.delayed(Duration(seconds: 1));
_navigationService.navigateTo(page1);
}
}
在page6里使用viewModel導航:
onPressed: () {
viewModel.goPage1();
},
現在,將View文件的職責帶回到了“顯示UI”并將用戶操作傳遞給模型,而不是“顯示UI”將用戶操作傳遞給模型并進行導航。更符合MVVM職責的劃分。
這樣做的好處是,隨著導航邏輯的擴展,我們的UI將保持不變,并且模型將承載所有邏輯/狀態管理。