下面的內容,僅當拋磚引玉,若是你有更好的實現思路,歡迎討論。是的,我來水文章了,今天要說的是在 Flutter 中, 如何設計一種通用的頁面路由。程序員
基本上,在大型的應用中,爲了幫助頁面與頁面之間的解耦,必定會提供路由的功能。所謂路由,在我看來其實就是一張 Hash Table,存放的是頁面的 Factory,經過這個 Factory,來建立頁面。bash
在 Flutter 中,經過實現 Scaffold 的組件,使得頁面具有導航能力。而 Navigator 則是其路由功能的一種實現。通常來講,你們均可以用這種方式在任意一個頁面組件中進行頁面跳轉。async
void click() {
Navigator.of(context).push('/login');
}
複製代碼
看起來,這種方式好像還不錯, push 返回的是一個 Future,當 login 頁面返回到主頁時,就會觸發這個 Future。然而, Navigator 提供了一個 pop ,那麼實際上, Navigator 就是須要你本身維護整個導航頁面的棧結構。函數
那麼,個人想法其實很簡單,就是提供一個統一的接口,傳入一個字符串來對這些頁面進行管理,而不是這種 push 和 pop 分散式的調用。動畫
我如今須要作的就是實現一個 Navigator 的封裝,而且本身維護一個棧結構。那麼首先要作的,就是先編寫一個全局的 Navigator ,這麼作的目的,是爲了編寫 context 無關的一個路由器。ui
class Router {
// 全局 Key
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
NavigatorState get _navigator => _navigatorKey.currentState;
}
複製代碼
經過 _navigator 就能夠實現一個簡單的 navigate 函數。this
Future<void> navigate<T>(Route<T> route) async {
await _navigator.push(route);
}
複製代碼
顯然,這個 navigate 就是在脫褲子放屁,這玩意顯然不符合個人想法。這裏,我參考了 Angular 的路由設計。先設計一個 Route,這個 Route 包含當前頁面組件,還有一些其餘別的內容,好比:路由守衛等。爲了方便,我先簡單設計一下這個類,因爲 Route 已經被命名,我也學一下尤雨溪,把它命名爲 Routage(法文:路由)。spa
class Routage {
final WidgetBuilder builder;
Routage({
this.builder,
}) : assert(builder != null);
}
複製代碼
而後,設計一個全局路由 Hash Table 。設計
final Map<String, Routage> routageTable = {
"/home": Routage(builder: (BuilderContext) => HomePage),
"/login": Routage(builder: (BuilderContext context) => LoginPage),
};
複製代碼
那麼, Router 就要持有這個 Hash Table 了。code
class Router {
final Map<String, Routage> _routageTable = routageTable;
// 全局 Key
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
NavigatorState get _navigator => _navigatorKey.currentState;
}
複製代碼
PageRoute 是 Flutter 提供的路由頁面抽象,他是一個抽象類,經過這個東西咱們能夠實現一些自定義動畫。可是對於大部分應用來講,跳轉的自定義動畫應該一早就該設計好的,而不是讓程序員自定義。因此,我認爲應該提供一個枚舉變量,來決定如何選用不一樣的 PageRoute 。
// rtl: right to left
// btu: bottom to up
enum NavigationStyle { rtl, btu }
class Router {
// 省略了...
Future<void> navigate<T>(String path, {
NavigationStyle style = NavigationStyle.rtl
}) async {
assert(style != null);
final routage = _routageTable[path];
final pageRoute = _pageRouteFrom(routage.builder, style);
await _navigator.push(pageRoute);
}
PageRoute _pageRouteFrom(WidgetBuilder builder, NavigationStyle style) {
if (style == NavigationStyle.rtl) {
return MaterialPageRoute(builder: builder, fullscreenDialog: true);
} else if (style == NavigationStyle.btu) {
return MaterialPageRoute(builder: builder);
} else {
return null;
}
}
// ...
}
複製代碼
好了,上面的代碼看起來很舒服,至少是有模有樣的,能夠知足個人想法。可是,它沒有合併操做,我每次調用這個方法,最終都會建立一個新頁面。在文章一開始我就說過,這種作法,須要本身維護一個狀態,那麼就開始吧。
class RouterState {
final List<String> stack = [];
String currentRoutage;
RouterState(this.currentRoutage);
// 判斷傳入的路由是否是當前的路由
bool isCurrent(String routage) {
return routage == currentRoutage;
}
// 判斷傳入的路由是否入棧
bool isContain(String routage) {
return stack.contains(routage);
}
// 傳入的路由入棧
void push(String routage) {
stack.add(currentRoutage);
currentRoutage = routage;
}
// 傳入的路由替換
void replace(String routage) {
stack.removeRange(0, stack.length);
currentRoutage = routage;
}
// 推出路由
void pop() {
currentRoutage = stack.last;
stack.removeLast();
}
}
複製代碼
結合上述的 RouterState ,合併了 push & pop 操做後的 navigate 函數以下。
Future<void> navigate<T>(
String path, {
NavigationStyle style = NavigationStyle.rtl,
}) async {
assert(style != null);
if (_state.isCurrent(path)) {
return;
}
if (_state.isContain(path)) {
while(_state.isContain(path)) {
_state.pop();
_navigator.pop();
}
return;
}
_state.push(path);
final routage = _routageTable[path];
final pageRoute = _pageRouteFrom(routage.builder, style);
await _navigator.push(pageRoute);
}
複製代碼
那麼,已經完善了嗎?我以爲還不夠,由於單純的 push 沒法實現頁面替換這種功能。我認爲,經過枚舉的傳入的方式來決定頁面變動的類型應該是對的。
enum NavigationType { normal, replace }
複製代碼
最終的 navigate 函數應該是這樣的。
Future<void> navigate<T>(
String path, {
NavigationStyle style = NavigationStyle.rtl,
NavigationType type = NavigationType.normal,
}) async {
assert(style != null);
assert(type != null);
if (_state.isCurrent(path)) {
return;
}
if (_state.isContain(path)) {
while(_state.isContain(path)) {
_state.pop();
_navigator.pop();
}
return;
}
if (type == NavigationType.normal) {
// 合併操做
_state.push(path);
final routage = _routageTable[path];
final pageRoute = _pageRouteFrom(routage.builder, style);
await _navigator.push(pageRoute);
} else if (type == NavigationType.replace) {
// 替換操做
_state.replace(path);
final routage = _routageTable[path];
final pageRoute = _pageRouteFrom(routage.builder, style);
await _navigator.pushAndRemoveUntil(pageRoute, (value) => value == null);
}
}
複製代碼
本文所介紹的方案相對直觀。實際操做過程當中,使用這個方案並無遇到什麼大問題,並且已經能解決我所遇到的大部分需求。我認爲,合併 push & pop,造成統一的路由接口這種設計方案未必是最佳的,可是,我目前並無遇到比這更好設計方案了。