路由管理
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。這是我們將用來執行所有路由的功能。
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將保持不變,並且模型將承載所有邏輯/狀態管理。