【Flutter】 介绍一种通用的页面路由设计方案

下面的内容,仅当抛砖引玉,若是你有更好的实现思路,欢迎讨论。是的,我来水文章了,今天要说的是在 Flutter 中, 如何设计一种通用的页面路由。程序员

基本上,在大型的应用中,为了帮助页面与页面之间的解耦,必定会提供路由的功能。所谓路由,在我看来其实就是一张 Hash Table,存放的是页面的 Factory,经过这个 Factory,来建立页面。bash

在 Flutter 中,经过实现 Scaffold 的组件,使得页面具有导航能力。而 Navigator 则是其路由功能的一种实现。通常来讲,你们均可以用这种方式在任意一个页面组件中进行页面跳转。async

void click() {
	Navigator.of(context).push('/login');
}
复制代码

看起来,这种方式好像还不错, push 返回的是一个 Future,当 login 页面返回到主页时,就会触发这个 Future。然而, Navigator 提供了一个 pop ,那么实际上, Navigator 就是须要你本身维护整个导航页面的栈结构。函数

那么,个人想法其实很简单,就是提供一个统一的接口,传入一个字符串来对这些页面进行管理,而不是这种 pushpop 分散式的调用。动画

路由器

我如今须要作的就是实现一个 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 映射

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,造成统一的路由接口这种设计方案未必是最佳的,可是,我目前并无遇到比这更好设计方案了。

相关文章
相关标签/搜索