我們通常會用屏(Screen)來稱呼一個頁面(Page),一個完整的App應該是有多個Page組成的。
在之前的案例(豆瓣)中,我們通過IndexedStack來管理了首頁中的Page切換:
首頁-書影音-小組-市集-我的
通過點選BottomNavigationBarItem來設定IndexedStack的index屬性來切換
除了上面這種管理頁面的方式,我們還需要實作其它功能的頁面跳轉:比如點選一個商品跳轉到詳情頁,某個按鈕跳轉到發送朋友圈、微網誌的編輯頁面。
這種頁面的管理和導航,我們通常會使用路由進行統一管理。
一. 路由管理
1.1. 認識Flutter路由
路由的概念由來已久,包括
網絡路由
、
後端路由
,到現在廣為流行的
前端路由
。
- 無論路由的概念如何應用,它的核心是一個
路由映射表
- 比如:名字
映射到detail
頁面等DetailPage
- 有了這個映射表之後,我們就可以友善的根據名字來完成路由的轉發(在前端表現出來的就是頁面跳轉)
在Flutter中,路由管理主要有兩個類:Route和Navigator
1.2. Route
Route:一個頁面要想被路由統一管理,必須包裝為一個Route
- 官方的說法很清晰:An abstraction for an entry managed by a Navigator .
但是Route是一個抽象類,是以它是不能執行個體化的
- 在上面有一段注釋,讓我們檢視MaterialPageRoute來使用
/// See [MaterialPageRoute] for a route that replaces the
/// entire screen with a platform-adaptive transition.
abstract class Route<T> {
}
事實上MaterialPageRoute并不是Route的直接子類:
- MaterialPageRoute在不同的平台有不同的表現
- 對Android平台,打開一個頁面會從螢幕底部滑動到螢幕的頂部,關閉頁面時從頂部滑動到底部消失
- 對iOS平台,打開一個頁面會從螢幕右側滑動到螢幕的左側,關閉頁面時從左側滑動到右側消失
- 當然,iOS平台我們也可以使用CupertinoPageRoute
MaterialPageRoute -> PageRoute -> ModalRoute -> TransitionRoute -> OverlayRoute -> Route
1.3. Navigator
Navigator:管理所有的Route的Widget,通過一個Stack來進行管理的
- 官方的說法也很清晰:A widget that manages a set of child widgets with a stack discipline.
那麼我們開發中需要手動去常見一個Navigator嗎?
- 并不需要,我們開發中使用的MaterialApp、CupertinoApp、WidgetsApp它們預設是有插入Navigator的
- 是以,我們在需要的時候,隻需要直接使用即可
Navigator.of(context)
Navigator有幾個最常見的方法:
// 路由跳轉:傳入一個路由對象
Future<T> push<T extends Object>(Route<T> route)
// 路由跳轉:傳入一個名稱(命名路由)
Future<T> pushNamed<T extends Object>(
String routeName, {
Object arguments,
})
// 路由傳回:可以傳入一個參數
bool pop<T extends Object>([ T result ])
二. 路由基本使用
1.1. 基本跳轉
我們來實作一個最基本跳轉:
- 建立首頁頁面,中間添加一個按鈕,點選按鈕跳轉到詳情頁面
- 建立詳情頁面,中間添加一個按鈕,點選按鈕傳回到首頁頁面
核心的跳轉代碼如下(首頁中代碼):
// RaisedButton代碼(隻貼出核心代碼)
RaisedButton(
child: Text("打開詳情頁"),
onPressed: () => _onPushTap(context),
),
// 按鈕點選執行的代碼
_onPushTap(BuildContext context) {
Navigator.of(context).push(MaterialPageRoute(
builder: (ctx) {
return DetailPage();
}
));
}
核心的傳回代碼如下(詳情頁中代碼):
// RaisedButton代碼(隻貼出核心代碼)
RaisedButton(
child: Text("傳回首頁"),
onPressed: () => _onBackTap(context),
)
// 按鈕點選執行的代碼
_onBackTap(BuildContext context) {
Navigator.of(context).pop();
}
1.2. 參數傳遞
在跳轉過程中,我們通常可能會攜帶一些參數,比如
- 首頁跳到詳情頁,攜帶一條資訊:a home message
- 詳情頁傳回首頁,攜帶一條資訊:a detail message
首頁跳轉核心代碼:
- 在頁面跳轉時,會傳回一個Future
- 該Future會在詳情頁面調用pop時,回調對應的then函數,并且會攜帶結果
_onPushTap(BuildContext context) {
// 1.跳轉代碼
final future = Navigator.of(context).push(MaterialPageRoute(
builder: (ctx) {
return DetailPage("a home message");
}
));
// 2.擷取結果
future.then((res) {
setState(() {
_message = res;
});
});
}
詳情頁傳回核心代碼:
_onBackTap(BuildContext context) {
Navigator.of(context).pop("a detail message");
}
1.3. 傳回細節
但是這裡有一個問題,如果使用者是點選右上角的傳回按鈕,如何監聽呢?
方法一:自定義傳回的按鈕(在詳情頁中修改Scaffold的appBar)
appBar: AppBar(
title: Text("詳情頁"),
leading: IconButton(
icon: Icon(Icons.arrow_back),
onPressed: () {
Navigator.of(context).pop("a back detail message");
},
),
),
方法二:監聽傳回按鈕的點選(給Scaffold包裹一個WillPopScope)
- WillPopScope有一個onWillPop的回調函數,當我們點選傳回按鈕時會執行
- 這個函數要求有一個Future的傳回值:
- true:那麼系統會自動幫我們執行pop操作
- false:系統不再執行pop操作,需要我們自己來執行
return WillPopScope(
onWillPop: () {
Navigator.of(context).pop("a back detail message");
return Future.value(false);
},
child: Scaffold(
appBar: AppBar(
title: Text("詳情頁"),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text("傳回首頁"),
onPressed: () => _onBackTap(context),
),
Text(_message, style: TextStyle(fontSize: 20, color: Colors.red),)
],
),
),
),
);
三. 命名路由使用
3.1. 基本跳轉
我們可以通過建立一個新的Route,使用Navigator來導航到一個新的頁面,但是如果在應用中很多地方都需要導航到同一個頁面(比如在開發中,首頁、推薦、分類頁都可能會跳到詳情頁),那麼就會存在很多重複的代碼。
在這種情況下,我們可以使用
命名路由(named route)- 命名路由是将名字和路由的映射關系,在一個地方進行統一的管理
- 有了命名路由,我們可以通過
方法來跳轉到新的頁面Navigator.pushNamed()
命名路由在哪裡管理呢?可以放在MaterialApp的
initialRoute
和
routes
中
-
:設定應用程式從哪一個路由開始啟動,設定了該屬性,就不需要再設定initialRoute
屬性了home
-
:定義名稱和路由之間的映射關系,類型為Map<String, WidgetBuilder>routes
修改MaterialApp中的代碼:
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue, splashColor: Colors.transparent
),
initialRoute: "/",
routes: {
"/home": (ctx) => HYHomePage(),
"/detail": (ctx) => HYDetailPage()
},
);
修改跳轉的代碼:
_onPushTap(BuildContext context) {
Navigator.of(context).pushNamed("/detail");
}
在開發中,為了讓每個頁面對應的routeName統一,我們通常會在每個頁面中定義一個路由的常量來使用:
class HYHomePage extends StatefulWidget {
static const String routeName = "/home";
}
class HYDetailPage extends StatelessWidget {
static const String routeName = "/detail";
}
修改MaterialApp中routes的key
initialRoute: HYHomePage.routeName,
routes: {
HYHomePage.routeName: (ctx) => HYHomePage(),
HYDetailPage.routeName: (ctx) => HYDetailPage()
},
3.2. 參數傳遞
因為通常命名路由,我們會在定義路由時,直接建立好對象,比如HYDetailPage()
那麼,命名路由如果有參數需要傳遞呢?
pushNamed時,如何傳遞參數:
_onPushTap(BuildContext context) {
Navigator.of(context).pushNamed(HYDetailPage.routeName, arguments: "a home message of naned route");
}
在HYDetailPage中,如何擷取到參數呢?
- 在build方法中ModalRoute.of(context)可以擷取到傳遞的參數
Widget build(BuildContext context) {
// 1.擷取資料
final message = ModalRoute.of(context).settings.arguments;
}
3.3. 路由鈎子
3.3.1. onGenerateRoute
加入我們有一個HYAboutPage,也希望在跳轉時,傳入對應的參數message,并且已經有一個對應的構造方法
在HYHomePage中添加跳轉的代碼:
RaisedButton(
child: Text("打開關于頁"),
onPressed: () {
Navigator.of(context).pushNamed(HYAboutPage.routeName, arguments: "a home message");
},
)
HYAboutPage的代碼:
class HYAboutPage extends StatelessWidget {
static const String routeName = "/about";
final String message;
HYAboutPage(this.message);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("關于頁面"),
),
body: Center(
child: Text(message, style: TextStyle(fontSize: 30, color: Colors.red),),
),
);
}
}
但是我們繼續使用routes中的映射關系,就不好進行配置了,因為HYAboutPage必須要求傳入一個參數;
這個時候我們可以使用onGenerateRoute的鈎子函數:
- 當我們通過pushNamed進行跳轉,但是對應的name沒有在routes中有映射關系,那麼就會執行onGenerateRoute鈎子函數;
- 我們可以在該函數中,手動建立對應的Route進行傳回;
- 該函數有一個參數RouteSettings,該類有兩個常用的屬性:
- name: 跳轉的路徑名稱
- arguments:跳轉時攜帶的參數
onGenerateRoute: (settings) {
if (settings.name == "/about") {
return MaterialPageRoute(
builder: (ctx) {
return HYAboutPage(settings.arguments);
}
);
}
return null;
},
3.3.2. onUnknownRoute
如果我們打開的一個路由名稱是根本不存在,這個時候我們希望跳轉到一個統一的錯誤頁面。
比如下面的abc是不存在有對應的頁面的
- 如果沒有進行特殊的處理,那麼Flutter會報錯。
RaisedButton(
child: Text("打開未知頁面"),
onPressed: () {
Navigator.of(context).pushNamed("/abc");
},
)
我們可以建立一個錯誤的頁面:
class UnknownPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("錯誤頁面"),
),
body: Container(
child: Center(
child: Text("頁面跳轉錯誤"),
),
),
);
}
}
并且設定onUnknownRoute
onUnknownRoute: (settings) {
return MaterialPageRoute(
builder: (ctx) {
return UnknownPage();
}
);
},
備注:所有内容首發于公衆号,之後除了Flutter也會更新其他技術文章,TypeScript、React、Node、uniapp、mpvue、資料結構與算法等等,也會更新一些自己的學習心得等,歡迎大家關注