【Flutter】 介紹一種通用的頁面路由設計方案

helloCat發表於2020-07-20

下面的內容,僅當拋磚引玉,如果你有更好的實現思路,歡迎討論。是的,我來水文章了,今天要說的是在 Flutter 中, 如何設計一種通用的頁面路由。

基本上,在大型的應用中,為了幫助頁面與頁面之間的解耦,一定會提供路由的功能。所謂路由,在我看來其實就是一張 Hash Table,存放的是頁面的 Factory,通過這個 Factory,來建立頁面。

在 Flutter 中,通過實現 Scaffold 的元件,使得頁面具備導航能力。而 Navigator 則是其路由功能的一種實現。一般來說,大家都可以用這種方式在任意一個頁面元件中進行頁面跳轉。

void click() {
	Navigator.of(context).push('/login');
}
複製程式碼

看起來,這種方式好像還不錯, push 返回的是一個 Future,當 login 頁面返回到主頁時,就會觸發這個 Future。然而, Navigator 提供了一個 pop ,那麼實際上, Navigator 就是需要你自己維護整個導航頁面的棧結構。

那麼,我的想法其實很簡單,就是提供一個統一的介面,傳入一個字串來對這些頁面進行管理,而不是這種 pushpop 分散式的呼叫。

路由器

我現在需要做的就是實現一個 Navigator 的封裝,並且自己維護一個棧結構。那麼首先要做的,就是先編寫一個全域性的 Navigator ,這麼做的目的,是為了編寫 context 無關的一個路由器。

class Router {
  // 全域性 Key
  final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
    
  NavigatorState get _navigator => _navigatorKey.currentState;
}
複製程式碼

通過 _navigator 就可以實現一個簡單的 navigate 函式。

Future<void> navigate<T>(Route<T> route) async {
  await _navigator.push(route);
}
複製程式碼

全域性路由

顯然,這個 navigate 就是在脫褲子放屁,這玩意顯然不符合我的想法。這裡,我參考了 Angular 的路由設計。先設計一個 Route,這個 Route 包含當前頁面元件,還有一些其他別的內容,比如:路由守衛等。為了方便,我先簡單設計一下這個類,由於 Route 已經被命名,我也學一下尤雨溪,把它命名為 Routage(法文:路由)。

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 了。

class Router {
  final Map<String, Routage> _routageTable = routageTable;
  // 全域性 Key
  final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
    
  NavigatorState get _navigator => _navigatorKey.currentState;
}
複製程式碼

PageRoute 對映

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();
  }
}
複製程式碼

合併 push & pop

結合上述的 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,形成統一的路由介面這種設計方案未必是最佳的,但是,我目前並沒有遇到比這更好設計方案了。

PS: 這種方案適合靜態路由。

相關文章