flutter 路由机制的实现

目录
  • 实现基础
  • _routeNamed
  • _flushHistoryUpdates
    • add
    • push
    • pop
    • remove
  • 总结

整个 flutter 应用的运行都只是基于原生应用中的一个 view,比如 android 中的 FlutterView,flutter 中的页面切换依赖于它的路由机制,也就是以 Navigator 为中心的一套路由功能,使得它能够完成与原生类似且能够自定义的页面切换效果。

下面将介绍 flutter 中的路由实现原理,包括初始化时的页面加载、切换页面的底层机制等。

实现基础

flutter 应用的运行需要依赖 MaterialApp/CupertinoApp 这两个 Widget,他们分别对应着 android/ios 的设计风格,同时也为应用的运行提供了一些基本的设施,比如与路由相关的主页面、路由表等,再比如跟整体页面展示相关的 theme、locale 等。

其中与路由相关的几项配置有 home、routes、initialRoute、onGenerateRoute、onUnknownRoute,它们分别对应着主页面 widget、路由表(根据路由找到对应 widget)、首次加载时的路由、路由生成器、未知路由代理(比如常见的 404 页面)。

MaterialApp/CupertinoApp 的子结点都是 WidgetsApp,只不过他们给 WidgetsApp 传入了不同的参数,从而使得两种 Widget 的界面风格不一致。Navigator 就是在 WidgetsApp 中创建的,

Widget build(BuildContext context) {
  Widget navigator;
    if (_navigator != null) {
    navigator = Navigator(
      key: _navigator,
      // If window.defaultRouteName isn't '/', we should assume it was set
      // intentionally via `setInitialRoute`, and should override whatever
      // is in [widget.initialRoute].
      initialRoute: WidgetsBinding.instance.window.defaultRouteName != Navigator.defaultRouteName
          ? WidgetsBinding.instance.window.defaultRouteName
          : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
      onGenerateRoute: _onGenerateRoute,
      onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
        ? Navigator.defaultGenerateInitialRoutes
        : (NavigatorState navigator, String initialRouteName) {
          return widget.onGenerateInitialRoutes(initialRouteName);
        },
      onUnknownRoute: _onUnknownRoute,
      observers: widget.navigatorObservers,
    );
  }
  ...
}

在 WidgetsApp 的 build 中第一个创建的就是 Navigator,主要看一下它的参数,首先,_navigator 是一个 GlobalKey,使得 WidgetsApp 可以通过 key 调用 Navigator 的函数进行路由切换,也就是在 WidgetsBinding 中处理 native 的路由切换信息的时候,最终是由 WidgetsApp 完成的。另外这里的 _navigator 应该只在 WidgetsApp 中有使用,其他地方需要使用一般是直接调用 Navigator.of 获取,这个函数会沿着 element 树向上查找到 NavigatorState,所以在应用中切换路由是需要被 Navigator 包裹的,不过由于 WidgetsApp 中都有生成 Navigator,开发中也不必考虑这些。

另外,就是关于底层获取上层 NavigatorElement 实例的方式,在 Element 树中有两种方式可以从底层获取到上层的实例,一种方式是使用 InheritedWidget,另一种就是直接沿着树向上查找(ancestorXXXOfExactType 系列),两种方式的原理基本是一致的,只不过 InheritedWidget 在建立树的过程中会一层层向下传递,而后者是使用的时候才向上查找,所以从这个角度来说使用 InheritedWidget 会高效些,但是 InheritedWidget 的优势不止如此,它是能够在数据发生改变的时候通知所有依赖它的结点进行更新,这也是 ancestorXXXOfExactType 系列所没有的。

然后 initialRoute 规定了初始化时候的页面,由 WidgetsBinding.instance.window.defaultRouteName 和 widget.initialRoute 来决定,不过前者优先级更高,因为这个是 native 中指定的,以 android 为例,在启动 FlutterActivity 的时候可以传入 route 字段指定初始化页面。

onGenerateRoute 和 onUnknownRoute 是获取 route 的策略,当 onGenerateRoute 没有命中时会调用 onUnknownRoute 给定一个默认的页面,onGenerateInitialRoutes 用于生产启动应用时的路由列表,它有一个默认实现 defaultGenerateInitialRoutes,会根据传递的 initialRouteName 选择不同的 Route,如果传入的 initialRouteName 并不是默认的主页面路由 Navigator.defaultRouteName,flutter 并不会将 initRoute 作为主页面,而是将默认路由入栈了之后再入栈 initRoute 对应的页面,所以如果在这之后再调用 popRoute,是会返回到主页面的

observers 是路由切换的监听列表,可以由外部传入,在路由切换的时候做些操作,比如 HeroController 就是一个监听者。
Navigator 是一个 StatefulWidget,在 NavigatorState 的 initState 中完成了将 initRoute 转换成 Route 的过程,并调用 push 将其入栈,生成 OverlayEntry,这个会继续传递给下层负责显示页面的 Overlay 负责展示。

在 push 的过程中,route 会被转换成 OverlayEntry 列表存放,每一个 OverlayEntry 中存储一个 WidgetBuilder,从某种角度来说,OverlayEntry 可以被认为是一个页面。所有的页面的协调、展示是通过 Overlay 完成的,Overlay 是一个类似于 Stack 的结构,它可以展示多个子结点。在它的 initState 中,

void initState() {
  super.initState();
  insertAll(widget.initialEntries);
}

会将 initialEntries 都存到 _entries 中。

Overlay 作为一个能够根据路由确定展示页面的控件,它的实现其实比较简单:

Widget build(BuildContext context) {
  // These lists are filled backwards. For the offstage children that
  // does not matter since they aren't rendered, but for the onstage
  // children we reverse the list below before adding it to the tree.
  final List<Widget> onstageChildren = <Widget>[];
  final List<Widget> offstageChildren = <Widget>[];
  bool onstage = true;
  for (int i = _entries.length - 1; i >= 0; i -= 1) {
    final OverlayEntry entry = _entries[i];
    if (onstage) {
      onstageChildren.add(_OverlayEntry(entry));
      if (entry.opaque)
        onstage = false;
    } else if (entry.maintainState) {
      offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry)));
    }
  }
  return _Theatre(
    onstage: Stack(
      fit: StackFit.expand,
      children: onstageChildren.reversed.toList(growable: false),
    ),
    offstage: offstageChildren,
  );
}

build 函数中,将所有的 OverlayEntry 分成了可见与不可见两部分,每一个 OverlayEntry 生成一个 _OverlayEntry,这是一个 StatefulWidget,它的作用主要是负责控制当前页重绘,都被封装成 然后再用  _Theatre 展示就完了,在 _Theatre 中,可见/不可见的子结点都会转成 Element,但是在绘制的时候,_Theatre 对应的 _RenderTheatre 只会把可见的子结点绘制出来。

判断某一个 OverlayEntry 是否能够完全遮挡上一个 OverlayEntry 是通过它的 opaque 变量判断的,而 opaque 又是由 Route 给出的,在页面动画执行时,这个值会被设置成 false,然后在页面切换动画执行完了之后就会把 Route 的 opaque 参数赋值给它的 OverlayEntry,一般情况下,窗口对应的 Route 为 false,页面对应的 Route 为 true。

所以说在页面切换之后,上一个页面始终都是存在于 element 树中的,只不过在 RenderObject 中没有将其绘制出来,这一点在 Flutter Outline 工具里面也能够体现。从这个角度也可以理解为,在 flutter 中页面越多,需要处理的步骤就越多,虽然不需要绘制底部的页面,但是整个树的基本遍历还是会有的,这部分也算是开销。

_routeNamed

flutter 中进行页面管理主要的依赖路由管理系统,它的入口就是 Navigator,它所管理的东西,本质上就是承载着用户页面的 Route,但是在 Navigator 中有很多函数是 XXXName 系列的,它们传的不是 Route,而是 RouteName,据个人理解,这个主要是方便开发引入的,我们可以在 MaterialApp/CupertinoApp 中直接传入路由表,每一个名字对应一个 WidgetBuilder,然后结合 pageRouteBuilder(这个可以自定义,不过 MaterialApp/CupertinoApp 都有默认实现,能够将 WidgetBuilder 转成 Route),便可以实现从 RouteName 到 Route 的转换。

Route<T> _routeNamed<T>(String name, { @required Object arguments, bool allowNull = false }) {
  if (allowNull && widget.onGenerateRoute == null)
    return null;
  final RouteSettings settings = RouteSettings(
    name: name,
    arguments: arguments,
  );
  Route<T> route = widget.onGenerateRoute(settings) as Route<T>;
  if (route == null && !allowNull) {
    route = widget.onUnknownRoute(settings) as Route<T>;
  }
  return route;
}

这个过程分三步,生成 RouteSettings,调用 onGenerateRoute 从路由表中拿到对应的路由,如果无命中,就调用 onUnknownRoute 给一个类似于 404 页面的东西。

onGenerateRoute 和 onUnknownRoute 在构建 Navigator 时传入,在 WidgetsApp 中实现,

Route<dynamic> _onGenerateRoute(RouteSettings settings) {
  final String name = settings.name;
  final WidgetBuilder pageContentBuilder = name == Navigator.defaultRouteName && widget.home != null
      ? (BuildContext context) => widget.home
      : widget.routes[name];
  if (pageContentBuilder != null) {
    final Route<dynamic> route = widget.pageRouteBuilder<dynamic>(
      settings,
      pageContentBuilder,
    );
    return route;
  }
  if (widget.onGenerateRoute != null)
    return widget.onGenerateRoute(settings);
  return null;
}

如果是默认的路由会直接使用给定的 home 页面(如果有),否则就直接到路由表查,所以本质上这里的 home 页面更多的是一种象征,身份的象征,没有也无所谓。另外路由表主要的产出是 WidgetBuilder,它需要经过一次包装,成为 Route 才是成品,或者如果不想使用路由表这种,也可以直接实现 onGenerateRoute 函数,根据 RouteSetting 直接生成 Route,这个就不仅仅是返回 WidgetBuilder 这么简单了,需要自己包装。

onUnknownRoute 主要用于兜底,提供一个类似于 404 的页面,它也是需要直接返回 Route。

_flushHistoryUpdates

不知道从哪一个版本开始,flutter 的路由管理引入了状态,与之前每一个 push、pop 都单独实现不同,所有的路由切换操作都是用状态表示,同时所有的 route 都被封装成 _RouteEntry,它内部有着关于 Route 操作的实现,但都被划分为比较小的单元,且都依靠状态来执行。

状态是一个具有递进关系的枚举,每一个 _RouteEntry 都有一个变量存放当前的状态,在 _flushHistoryUpdates 中会遍历所有的 _RouteEntry 然后根据它们当前的状态进行处理,同时处理完成之后会切换它们的状态,再进行其他处理,这样的好处很明显,所有的路由都放在一起处理之后,整个流程会变得更加清晰,且能够很大程度上进行代码复用,比如 push 和 pushReplacement 两种操作,这在之前是需要在两个方法中单独实现的,而现在他们则可以放在一起单独处理,不同的只有后者比前者会多一个 remove 的操作。

关于 _flushHistoryUpdates 的处理步骤:

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
  assert(_debugLocked && !_debugUpdatingPage);
  // Clean up the list, sending updates to the routes that changed. Notably,
  // we don't send the didChangePrevious/didChangeNext updates to those that
  // did not change at this point, because we're not yet sure exactly what the
  // routes will be at the end of the day (some might get disposed).
  int index = _history.length - 1;
  _RouteEntry next;
  _RouteEntry entry = _history[index];
  _RouteEntry previous = index > 0 ? _history[index - 1] : null;
  bool canRemoveOrAdd = false; // Whether there is a fully opaque route on top to silently remove or add route underneath.
  Route<dynamic> poppedRoute; // The route that should trigger didPopNext on the top active route.
  bool seenTopActiveRoute = false; // Whether we've seen the route that would get didPopNext.
  final List<_RouteEntry> toBeDisposed = <_RouteEntry>[];
  while (index >= 0) {
    switch (entry.currentState) {
        // ...
    }
    index -= 1;
    next = entry;
    entry = previous;
    previous = index > 0 ? _history[index - 1] : null;
  }
  // Now that the list is clean, send the didChangeNext/didChangePrevious
  // notifications.
  _flushRouteAnnouncement();
  // Announces route name changes.
  final _RouteEntry lastEntry = _history.lastWhere(_RouteEntry.isPresentPredicate, orElse: () => null);
  final String routeName = lastEntry?.route?.settings?.name;
  if (routeName != _lastAnnouncedRouteName) {
    RouteNotificationMessages.maybeNotifyRouteChange(routeName, _lastAnnouncedRouteName);
    _lastAnnouncedRouteName = routeName;
  }
  // Lastly, removes the overlay entries of all marked entries and disposes
  // them.
  for (final _RouteEntry entry in toBeDisposed) {
    for (final OverlayEntry overlayEntry in entry.route.overlayEntries)
      overlayEntry.remove();
    entry.dispose();
  }
  if (rearrangeOverlay)
    overlay?.rearrange(_allRouteOverlayEntries);
}

以上是除了状态处理之外,一次 _flushHistoryUpdates 的全过程,首先它会遍历整个路由列表,根据状态做不同的处理,不过一般能够处理到的也不过最上层一两个,其余的多半是直接跳过的。处理完了之后,调用 _flushRouteAnnouncement 进行路由之间的前后链接,比如进行动画的联动等,

void _flushRouteAnnouncement() {
  int index = _history.length - 1;
  while (index >= 0) {
    final _RouteEntry entry = _history[index];
    if (!entry.suitableForAnnouncement) {
      index -= 1;
      continue;
    }
    final _RouteEntry next = _getRouteAfter(index + 1, _RouteEntry.suitableForTransitionAnimationPredicate);
    if (next?.route != entry.lastAnnouncedNextRoute) {
      if (entry.shouldAnnounceChangeToNext(next?.route)) {
        entry.route.didChangeNext(next?.route);
      }
      entry.lastAnnouncedNextRoute = next?.route;
    }
    final _RouteEntry previous = _getRouteBefore(index - 1, _RouteEntry.suitableForTransitionAnimationPredicate);
    if (previous?.route != entry.lastAnnouncedPreviousRoute) {
      entry.route.didChangePrevious(previous?.route);
      entry.lastAnnouncedPreviousRoute = previous?.route;
    }
    index -= 1;
  }
}

其实现也比较清晰,对每一个 _RouteEntry,通过调用 didChangeNext 和 didChangePrevious 来建立联系,比如在 didChangeNext 中绑定当前 Route 的 secondaryAnimation 和下一个路由的 animation 进行动画联动,再比如在 didChangePrevious 中获取上一个路由的 title,这个可以用于 CupertinoNavigationBar 中 back 按钮展示上一页面的 title。
然后调用 maybeNotifyRouteChange 发出通知,指定当前正在处于展示状态的 Route。

最后,遍历 toBeDisposed 执行 _RouteEntry 的销毁,这个列表会保存上面循环处理过程中,确定需要移出的 _RouteEntry,通过调用 OverlayEntry remove 函数(它会将自己从 Overlay 中移除)和 OverlayEntry dispose 函数(它会调用 Route 的 dispose,进行资源释放,比如 TransitionRoute 中 AnimationController 销毁)。

最后再看关于状态的处理,以下是所有的状态:

enum _RouteLifecycle {
  staging, // we will wait for transition delegate to decide what to do with this route.
  //
  // routes that are present:
  //
  add, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages
  adding, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages
  // routes that are ready for transition.
  push, // we'll want to run install, didPush, etc; a route added via push() and friends
  pushReplace, // we'll want to run install, didPush, etc; a route added via pushReplace() and friends
  pushing, // we're waiting for the future from didPush to complete
  replace, // we'll want to run install, didReplace, etc; a route added via replace() and friends
  idle, // route is being harmless
  //
  // routes that are not present:
  //
  // routes that should be included in route announcement and should still listen to transition changes.
  pop, // we'll want to call didPop
  remove, // we'll want to run didReplace/didRemove etc
  // routes should not be included in route announcement but should still listen to transition changes.
  popping, // we're waiting for the route to call finalizeRoute to switch to dispose
  removing, // we are waiting for subsequent routes to be done animating, then will switch to dispose
  // routes that are completely removed from the navigator and overlay.
  dispose, // we will dispose the route momentarily
  disposed, // we have disposed the route
}

本质上这些状态分为三类,add(处理初始化的时候直接添加),push(与 add 类似,但是增加了动画的处理),pop(处理页面移出),remove(移出某个页面,相对 pop 没有动画,也没有位置限制)。

add

add 方式添加路由目前还只用于在应用初始化是添加初始化页面使用,对应的是在 NavigatorState 的 initState 中,

void initState() {
  super.initState();
  for (final NavigatorObserver observer in widget.observers) {
    assert(observer.navigator == null);
    observer._navigator = this;
  }
  String initialRoute = widget.initialRoute;
  if (widget.pages.isNotEmpty) {
    _history.addAll(
      widget.pages.map((Page<dynamic> page) => _RouteEntry(
        page.createRoute(context),
        initialState: _RouteLifecycle.add,
      ))
    );
  } else {
    // If there is no page provided, we will need to provide default route
    // to initialize the navigator.
    initialRoute = initialRoute ?? Navigator.defaultRouteName;
  }
  if (initialRoute != null) {
    _history.addAll(
      widget.onGenerateInitialRoutes(
        this,
        widget.initialRoute ?? Navigator.defaultRouteName
      ).map((Route<dynamic> route) =>
        _RouteEntry(
          route,
          initialState: _RouteLifecycle.add,
        ),
      ),
    );
  }
  _flushHistoryUpdates();
}

它会将从 onGenerateInitialRoutes 得来的所有初始路由转成 _RouteEntry 加入到 _history,此时它们的状态是 _RouteLifecycle.add,然后就是调用 _flushHistoryUpdates 进行处理。

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
  // ...
  while (index >= 0) {
    switch (entry.currentState) {
      case _RouteLifecycle.add:
        assert(rearrangeOverlay);
        entry.handleAdd(
          navigator: this,
        );
        assert(entry.currentState == _RouteLifecycle.adding);
        continue;
      case _RouteLifecycle.adding:
        if (canRemoveOrAdd || next == null) {
          entry.didAdd(
            navigator: this,
            previous: previous?.route,
            previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
            isNewFirst: next == null
          );
          assert(entry.currentState == _RouteLifecycle.idle);
          continue;
        }
        break;
      case _RouteLifecycle.idle:
        if (!seenTopActiveRoute && poppedRoute != null)
          entry.handleDidPopNext(poppedRoute);
        seenTopActiveRoute = true;
        // This route is idle, so we are allowed to remove subsequent (earlier)
        // routes that are waiting to be removed silently:
        canRemoveOrAdd = true;
        break;
        // ...
    }
    index -= 1;
    next = entry;
    entry = previous;
    previous = index > 0 ? _history[index - 1] : null;
  }
  // ...
}

add 路线主要会调用两个函数,handleAdd 和 didAdd,

void handleAdd({ @required NavigatorState navigator}) {
  assert(currentState == _RouteLifecycle.add);
  assert(navigator != null);
  assert(navigator._debugLocked);
  assert(route._navigator == null);
  route._navigator = navigator;
  route.install();
  assert(route.overlayEntries.isNotEmpty);
  currentState = _RouteLifecycle.adding;
}

install 函数可以看作是 Route 的初始化函数,比如在 ModalRoute 中创建 ProxyAnimation 来管理一些动画的执行,在 TransitionRoute 中创建了用于执行切换动画的 AnimationController,在 OverlayRoute 中完成了当前 Route 的 OverlayEntry 的创建及插入。createOverlayEntries 用于创建 OverlayEntry,其实现在 ModalRoute,

Iterable<OverlayEntry> createOverlayEntries() sync* {
  yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
  yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
}

每一个 Route 都能生成两个 OverlayEntry,一个是 _buildModalBarrier,它可以生成两个页面之间的屏障,我们可以利用它给新页面设置一个背景色,同时还支持动画过渡,另一个是 _buildModalScope,它生成的就是这个页面真正的内容,外部会有多层包装,最底层就是 WidgetBuilder 创建的 widget。

大致看下两个函数的实现,

Widget _buildModalBarrier(BuildContext context) {
  Widget barrier;
  if (barrierColor != null && !offstage) { // changedInternalState is called if these update
    assert(barrierColor != _kTransparent);
    final Animation<Color> color = animation.drive(
      ColorTween(
        begin: _kTransparent,
        end: barrierColor, // changedInternalState is called if this updates
      ).chain(_easeCurveTween),
    );
    barrier = AnimatedModalBarrier(
      color: color,
      dismissible: barrierDismissible, // changedInternalState is called if this updates
      semanticsLabel: barrierLabel, // changedInternalState is called if this updates
      barrierSemanticsDismissible: semanticsDismissible,
    );
  } else {
    barrier = ModalBarrier(
      dismissible: barrierDismissible, // changedInternalState is called if this updates
      semanticsLabel: barrierLabel, // changedInternalState is called if this updates
      barrierSemanticsDismissible: semanticsDismissible,
    );
  }
  return IgnorePointer(
    ignoring: animation.status == AnimationStatus.reverse || // changedInternalState is called when this updates
              animation.status == AnimationStatus.dismissed, // dismissed is possible when doing a manual pop gesture
    child: barrier,
  );
}

ModalBarrier 是两个 Route 之间的屏障,它可以通过颜色、拦截事件来表示两个 Route 的隔离,这些都是可以配置的,这里 IgnorePointer 的作用是为了在执行切换动画的时候无法响应时间。

Widget _buildModalScope(BuildContext context) {
  return _modalScopeCache ??= _ModalScope<T>(
    key: _scopeKey,
    route: this,
    // _ModalScope calls buildTransitions() and buildChild(), defined above
  );
}

Widget build(BuildContext context) {
  return _ModalScopeStatus(
    route: widget.route,
    isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
    canPop: widget.route.canPop, // _routeSetState is called if this updates
    child: Offstage(
      offstage: widget.route.offstage, // _routeSetState is called if this updates
      child: PageStorage(
        bucket: widget.route._storageBucket, // immutable
        child: FocusScope(
          node: focusScopeNode, // immutable
          child: RepaintBoundary(
            child: AnimatedBuilder(
              animation: _listenable, // immutable
              builder: (BuildContext context, Widget child) {
                return widget.route.buildTransitions(
                  context,
                  widget.route.animation,
                  widget.route.secondaryAnimation,
                  IgnorePointer(
                    ignoring: widget.route.animation?.status == AnimationStatus.reverse,
                    child: child,
                  ),
                );
              },
              child: _page ??= RepaintBoundary(
                key: widget.route._subtreeKey, // immutable
                child: Builder(
                  builder: (BuildContext context) {
                    return widget.route.buildPage(
                      context,
                      widget.route.animation,
                      widget.route.secondaryAnimation,
                    );
                  },
                ),
              ),
            ),
          ),
        ),
      ),
    ),
  );
}

_ModalScope 需要承载用户界面的展示,它的 build 函数可以看到在 widget.route.buildPage 出用户定义的页面之上有很多层,可以一层一层看下大致作用:

  • _ModalScopeStatus,继承自 InheritedWidget,用于给底层结点提供数据
  • Offstage,可以通过 offstage 变量控制是否绘制
  • PageStorage,它提供了一种存储策略,也就是 PageStorageBucket,这个类可以给某一个 BuildContext 绑定特定的数据,支持写入和读取,可用于某一个 widget 的状态存储等
  • FocusScope,用于焦点管理用,一般只有获取焦点的控件才能接收到按键信息等
  • RepaintBoundary,控制重绘范围,意在减少不必要的重绘
  • AnimatedBuilder,动画控制 Widget,会根据 animation 进行 rebuild
  • widget.route.buildTransitions,它在不同的 Route 中可以有不同的实现,比如 Android 的默认实现是自下向上渐入,ios 的默认实现是自右向左滑动,另外也可以通过自定义 Route 或自定义 ThemeData 实现自定义的切换动画,还有一点需要说明,Route 中的动画分为 animation 和 secondaryAnimation,其中 animation 定义了自己 push 时的动画,secondaryAnimation 定义的是新页面 push 时自己的动画,举个例子,在 ios 风格中,新页面自右向左滑动,上一个页面也会滑动,此时控制上一个页面滑动的动画就是 secondaryAnimation
  • IgnorePointer,同样是用于页面切换动画执行中,禁止用户操作
  • RepaintBoundary,这里的考量应该是考虑到上层有一个动画执行,所以这里包一下避免固定内容重绘
  • Builder,Builder 的唯一作用应该是提供 BuildContext,虽然说每一个 build 函数都有 BuildContext 参数,但这个是当前 Widget 的,而不是直属上级的,这可能有点抽象,比如说下面的 buildPage 需要使用 BuildContext 作为参数,那么如果它需要使用 context 的 ancestorStateOfType 的话,实际上就是从 _ModalScopeState 开始向上查找,而不是从 Builder 开始向上查找
  • widget.route.buildPage,这个函数内部就是使用 Route 的 WidgetBuilder 创建用户界面,当然不同的 Route 可能还会在这里再次进行包装

以上就是一个页面中,从 Overlay(说是 Overlay 不是那么合理,但是在此先省略中间的 _Theatre 等) 往下的布局嵌套。新的 OverlayEntry 创建完成之后,会把它们都传递到 Overlay 中,且在这个过程中会调用 Overlay 的 setState 函数,请求重新绘制,在 Overlay 中实现新旧页面的切换。

以上是 install 的整个过程,执行完了之后把 currentState 置为 adding 返回。

此处有一点需要注意,while 循环会自上往下遍历所有的 _RouteEntry,但是当一个连续操作尚未完成时,它是不会去执行下一个 _RouteEntry 的,其实现就在于代码中的 continue 关键字,这个关键字会直接返回执行下一次循环,但是并没有更新当前 _RouteEntry,所以实际处理的还是同一个路由,这种一般用于 _RouteEntry 状态发生变化,且需要连续处理的时候,所以对于 add 来说,执行完了之后会立刻执行 adding 代码块,也就是 didAdd,

void didAdd({ @required NavigatorState navigator, @required bool isNewFirst, @required Route<dynamic> previous, @required Route<dynamic> previousPresent }) {
  route.didAdd();
  currentState = _RouteLifecycle.idle;
  if (isNewFirst) {
    route.didChangeNext(null);
  }
  for (final NavigatorObserver observer in navigator.widget.observers)
    observer.didPush(route, previousPresent);
}

Route 的 didAdd 函数表示这个路由已经添加完成,它会做一些收尾处理,比如在 TransitionRoute 中更新 AnimationController 的值到最大,并设置透明等。然后 didAdd 将状态置为 idle,并调用所有监听者的 didPush。idle 表示一个 _RouteEntry 已经处理完毕,后续只有 pop、replace 等操作才会需要重新处理,add 过程到这里也可以结束了。

push

Future<T> push<T extends Object>(Route<T> route) {
  assert(!_debugLocked);
  assert(() {
    _debugLocked = true;
    return true;
  }());
  assert(route != null);
  assert(route._navigator == null);
  _history.add(_RouteEntry(route, initialState: _RouteLifecycle.push));
  _flushHistoryUpdates();
  assert(() {
    _debugLocked = false;
    return true;
  }());
  _afterNavigation(route);
  return route.popped;
}

push 过程就是将 Route 封装成 _RouteEntry 加入到 _history 中并调用 _flushHistoryUpdates,它的初始状态时 push,并在最后返回 route.popped,这是一个 Future 对象,可以用于前一个页面接收新的页面的返回结果,这个值是在当前路由 pop 的时候传递的。

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
  // ...
  while (index >= 0) {
    switch (entry.currentState) {
      // ...
      case _RouteLifecycle.push:
      case _RouteLifecycle.pushReplace:
      case _RouteLifecycle.replace:
        assert(rearrangeOverlay);
        entry.handlePush(
          navigator: this,
          previous: previous?.route,
          previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
          isNewFirst: next == null,
        );
        assert(entry.currentState != _RouteLifecycle.push);
        assert(entry.currentState != _RouteLifecycle.pushReplace);
        assert(entry.currentState != _RouteLifecycle.replace);
        if (entry.currentState == _RouteLifecycle.idle) {
          continue;
        }
        break;
      case _RouteLifecycle.pushing: // Will exit this state when animation completes.
        if (!seenTopActiveRoute && poppedRoute != null)
          entry.handleDidPopNext(poppedRoute);
        seenTopActiveRoute = true;
        break;
      case _RouteLifecycle.idle:
        if (!seenTopActiveRoute && poppedRoute != null)
          entry.handleDidPopNext(poppedRoute);
        seenTopActiveRoute = true;
        // This route is idle, so we are allowed to remove subsequent (earlier)
        // routes that are waiting to be removed silently:
        canRemoveOrAdd = true;
        break;
      // ...
    }
    index -= 1;
    next = entry;
    entry = previous;
    previous = index > 0 ? _history[index - 1] : null;
  }
  // ...
}

这里将 push、pushReplace、replace 都归为了一类,它会先调用 handlePush,这个函数中其实包含了 add 过程中的 handleAdd、didAdd 两个函数的功能,比如调用 install、调用 didPush,不同的是,push/pushReplace 会有一个过渡的过程,即先执行切换动画,此时它的状态会变为 pushing,并在动画执行完时切到 idle 状态并调用 _flushHistoryUpdates 更新,而 replace 则直接调用 didReplace 完成页面替换,从这里看,这个应该是没有动画过渡的。后面还是一样,调用通知函数。

pop

pop 的过程与上面两个不太一样,它在 NavigatorState.pop 中也有一些操作:

void pop<T extends Object>([ T result ]) {
  assert(!_debugLocked);
  assert(() {
    _debugLocked = true;
    return true;
  }());
  final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate);
  if (entry.hasPage) {
    if (widget.onPopPage(entry.route, result))
      entry.currentState = _RouteLifecycle.pop;
  } else {
    entry.pop<T>(result);
  }
  if (entry.currentState == _RouteLifecycle.pop) {
    // Flush the history if the route actually wants to be popped (the pop
    // wasn't handled internally).
    _flushHistoryUpdates(rearrangeOverlay: false);
    assert(entry.route._popCompleter.isCompleted);
  }
  assert(() {
    _debugLocked = false;
    return true;
  }());
  _afterNavigation<dynamic>(entry.route);
}

就是调用 _RouteEntry 的 pop,在这个函数中它会调用 Route 的 didPop,完成返回值的传递、移出动画启动等。但是在  OverlayRoute 中:

bool didPop(T result) {
  final bool returnValue = super.didPop(result);
  assert(returnValue);
  if (finishedWhenPopped)
    navigator.finalizeRoute(this);
  return returnValue;
}

finalizeRoute 的调用需要依赖 finishedWhenPopped 的值,这个值在子类中可以被修改,比如 TransitionRoute 中它就是 false,理解也很简单,在 TransitionRoute 中执行 didPop 之后也不能直接就销毁 Route,而是先要执行移出动画,而如果不需要执行动画,则可以直接调用,否则就在动画执行完再执行,这一点是通过监听动画状态实现的,在 TransitionRoute 中。

void finalizeRoute(Route<dynamic> route) {
  // FinalizeRoute may have been called while we were already locked as a
  // responds to route.didPop(). Make sure to leave in the state we were in
  // before the call.
  bool wasDebugLocked;
  assert(() { wasDebugLocked = _debugLocked; _debugLocked = true; return true; }());
  assert(_history.where(_RouteEntry.isRoutePredicate(route)).length == 1);
  final _RouteEntry entry =  _history.firstWhere(_RouteEntry.isRoutePredicate(route));
  if (entry.doingPop) {
    // We were called synchronously from Route.didPop(), but didn't process
    // the pop yet. Let's do that now before finalizing.
    entry.currentState = _RouteLifecycle.pop;
    _flushHistoryUpdates(rearrangeOverlay: false);
  }
  assert(entry.currentState != _RouteLifecycle.pop);
  entry.finalize();
  _flushHistoryUpdates(rearrangeOverlay: false);
  assert(() { _debugLocked = wasDebugLocked; return true; }());
}

在 finalizeRoute 中,它会判断是否正在 pop 过程中,如果是,就说明此刻是直接调用的 finalizeRoute,那就需要先执行 pop 状态的操作,再执行 dispose 操作,将状态切换到 dispose 进行处理,如果不是,就说明调用这个函数的时候,是动画执行完的时候,那么此刻 pop 状态处理已经完成,所以跳过了 pop 处理的步骤,如上。下面就看一下 pop 过程做的处理。

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
  // ...
  while (index >= 0) {
    switch (entry.currentState) {
      // ...
      case _RouteLifecycle.pop:
        if (!seenTopActiveRoute) {
          if (poppedRoute != null)
            entry.handleDidPopNext(poppedRoute);
          poppedRoute = entry.route;
        }
        entry.handlePop(
          navigator: this,
          previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
        );
        assert(entry.currentState == _RouteLifecycle.popping);
        canRemoveOrAdd = true;
        break;
      case _RouteLifecycle.popping:
        // Will exit this state when animation completes.
        break;
      case _RouteLifecycle.dispose:
        // Delay disposal until didChangeNext/didChangePrevious have been sent.
        toBeDisposed.add(_history.removeAt(index));
        entry = next;
        break;
      case _RouteLifecycle.disposed:
      case _RouteLifecycle.staging:
        assert(false);
        break;
    }
    index -= 1;
    next = entry;
    entry = previous;
    previous = index > 0 ? _history[index - 1] : null;
  }
  // ...
}

handlePop 将状态切换到 poping(动画执行过程),然后发出通知,而 poping 状态不作处理,因为这是一个过渡状态,在动画执行完之后会自动切换到 dispose 状态,同样的,上面的 pushing 状态也是,而在 dispose 分支中,就是将 _RouteEntry 从 _history 移除并加入到 toBeDisposed,然后在遍历结束之后统一销毁。

remove

remove 的逻辑就是先从 _history 中找到一个跟传进来的一致的 _RouteEntry,将它的状态设为 remvoe,再调用 _flushHistoryUpdates。

void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
  // ...
  while (index >= 0) {
    switch (entry.currentState) {
        // ...
      case _RouteLifecycle.remove:
        if (!seenTopActiveRoute) {
          if (poppedRoute != null)
            entry.route.didPopNext(poppedRoute);
          poppedRoute = null;
        }
        entry.handleRemoval(
          navigator: this,
          previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
        );
        assert(entry.currentState == _RouteLifecycle.removing);
        continue;
      case _RouteLifecycle.removing:
        if (!canRemoveOrAdd && next != null) {
          // We aren't allowed to remove this route yet.
          break;
        }
        entry.currentState = _RouteLifecycle.dispose;
        continue;
      case _RouteLifecycle.dispose:
        // Delay disposal until didChangeNext/didChangePrevious have been sent.
        toBeDisposed.add(_history.removeAt(index));
        entry = next;
        break;
      case _RouteLifecycle.disposed:
      case _RouteLifecycle.staging:
        assert(false);
        break;
    }
    index -= 1;
    next = entry;
    entry = previous;
    previous = index > 0 ? _history[index - 1] : null;
  }
  // ...
}

首先会调用 handleRemoval,调用通知,并将状态切换到 removing,在 removing 阶段再将状态切到 dispose,然后就是将其加入 toBeDisposed,所以整个过程中是不涉及动画的,一般只用来移出非正在展示的页面,否则还是推荐用 pop。

总结

以上是路由机制的实现原理,就其整体而言,最给人耳目一新的就是状态管理的加入,通过将一个页面的进出划分到不同状态处理,是能够有效降低代码的复杂度的,不过从目前的结果来看,这一个过程执行的还不够精炼,比如状态的划分不够合理,从这些状态的设计来看,add/push/pop 都有对应的 ing 形式表示正在执行中,但是 adding 的存在我暂时没有看到必要性,还有就是感觉代码的组织上还是有点问题,比如 handleAdd 与 handPush 实际上还有很大部分的代码重复的,这部分不知道以后会不会优化。

另外还有一点感觉做的不到位,就是 _routeNamed 这个函数没有对外开放,而且并不是所有的路由操作都提供了 name 为入参的包装,比如 removeRoute,在这种情况下就没法很方便的调用。

到此这篇关于flutter 路由机制的实现的文章就介绍到这了,更多相关flutter 路由机制内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Flutter 路由插件fluro的使用

    前面两篇文章我们介绍了Flutter 的原生导航器 Navigator 实现页面跳转,路由及路由拦截的使用,具体可以参考之前的文章: //www.jb51.net/article/215167.htm //www.jb51.net/article/214856.htm 使用原生的路由基本上能够满足大部分需求,但如果想要对页面做类似浏览器 url 那样的路由,或者控制页面跳转的转场动画,那么原生的路由需要做不少的改造.在 pub 上,有优秀的路由插件 fluro 解决这类问题. fluro的使用方

  • Flutter开发之路由与导航的实现

    如果说构成视图元素的基本单位是组件,那么构成应用程序的基本单位就是页面.对于拥有多个页面的应用程序而言,如何从一个页面平滑地过渡到另一个页面,是技术框架需要考虑的问题. 在前端开发中,可以使用路由框架来统一管理页面及它们之间的跳转.在Android中路由指的是一个Activity,在iOS中指的是一个ViewController,可以通过startActivity或pushViewController来打开一个新的路由.在Flutter中,路由的管理和导航借鉴了前端和客户端的设计思路,需要使用R

  • Flutter开发中的路由参数处理

    目录 Navigator 的 push 和 pop方法 Navigator 导航器的 push 和 pop 方法可以携带参数在页面间传递,其他变形的方法也一样.pushNamed 方法原型如下: Future<T?> pushNamed<T extends Object?>( String routeName, { Object? arguments, }) { return push<T>(_routeNamed<T>(routeName, argumen

  • 详解Flutter的路由导航

    Flutter 的路由导航 路由管理或导航管理:从一个页面平滑地过渡到另一个页面,我们需要有一个统一的机制来管理页面之间的跳转.在原生的Android 开发,是通过startActivity或startActivityForResult 来完成页面的跳转的,在Flutter 中如何实现呢? 在 Flutter 中,页面之间的跳转是通过 Route 和 Navigator 来管理的: Route 是页面的抽象,主要负责创建对应的界面,接收参数,响应 Navigator 打开和关闭: 而 Navig

  • flutter 路由跳转的实现示例

    路由 做Android/iOS原生开发的时候,要打开一个新的页面,你得知道你的目标页面对象,然后初始化一个Intent或者ViewController,再通过startActivity或者pushViewController来推出一个新的页面,不能跟web一样,直接丢一个链接地址就跳转到新的页面.当然,可以自己去加一个中间层来实现这些功能. Flutter里面是原生支持路由的.Flutter的framework提供了路由跳转的实现.我们可以直接使用这些功能. Flutter路由介绍 Flutte

  • Flutter 局部路由实现详解

    Flutter是借鉴React的开发思想实现的,在子组件的插槽上,React有this.props.children,Vue有<slot></slot>. 当然Flutter也有类似的Widget,那就是Navigator,不过是以router的形式实现(像<router-view></router-view>???). Navigator的使用无非3个属性 initialRoute: 初始路由 onGenerateRoute: 匹配路由 onUnknown

  • flutter 路由机制的实现

    目录 实现基础 _routeNamed _flushHistoryUpdates add push pop remove 总结 整个 flutter 应用的运行都只是基于原生应用中的一个 view,比如 android 中的 FlutterView,flutter 中的页面切换依赖于它的路由机制,也就是以 Navigator 为中心的一套路由功能,使得它能够完成与原生类似且能够自定义的页面切换效果. 下面将介绍 flutter 中的路由实现原理,包括初始化时的页面加载.切换页面的底层机制等. 实

  • Flutter路由fluro引入配置和使用的具体方法

    目录 flutter_fluro简介 引入fluro 初始化Fluro 编写rotuer_handler 配置路由 把Fluro的Router静态化 把路由注册/注入到顶层 在首页使用 总结: Flutter本身提供了路由机制,作个人的小型项目,完全足够了.但是如果你要作企业级开发,可能就会把入口文件变得臃肿不堪.而再Flutter问世之初,就已经了企业级路由方案fluro. flutter_fluro简介 fluro简化了Flutter的路由开发,也是目前Flutter生态中最成熟的路由框架.

  • AngularJS入门教程之路由机制ngRoute实例分析

    本文实例讲述了AngularJS路由机制ngRoute.分享给大家供大家参考,具体如下: 引言 在我们介绍路由之前我们首先谈一下SPA,所以SPA就是我们现在经常说的单页应用single page APP,为了实现无刷新的视图切换我们之前的做法就是利用AJAX从后取出数据然后渲染在前台页面HTML中,但是AJAX有一个致命的缺点就是不能实现浏览器的后退按钮失效,为了解决这个问题我们通常使用hash,监听hashchange事件来进行视图切换,另一个方法是用HTML5的history API,通过

  • Zend Framework框架路由机制代码分析

    本文分析了Zend Framework框架路由机制代码.分享给大家供大家参考,具体如下: 在框架中,有关路由的调用关系为: 1.apache的mod_rewrite模块把请求路由到框架的启动脚本,一般是index.php: 2.前端控制器Zend_Controller_Front通过dispatch函数进行请求分发: 3.路由器Zend_Controller_Router_Rewrite通过route函数处理路由,对路由器中已有的路由规则,按照加入顺序的逆序(类似于栈,后进先出)对每个route

  • ThinkPHP路由机制简介

    本文实例讲述了ThinkPHP路由机制.分享给大家供大家参考,具体如下: ThinkPHP 支持 URL 路由功能,要启用路由功能,需要设置ROUTER_ON参数为true.开启路由功能后,系统会自动进行路由检测,如果在路由定义里面找到和当前URL匹配的路由名称,就会进行路由解析和重定向.路由功能需要定义路由定义文件,位于项目的配置目录下面,文件名为 routes.php 定义格式: Return Array( 'RouteName'=>array('模块名称','操作名称','参数定义','额

  • Flutter路由的跳转、动画和传参详解(最简单)

    路由 做Android/iOS原生开发的时候,要打开一个新的页面,你得知道你的目标页面对象,然后初始化一个Intent或者ViewController,再通过startActivity或者pushViewController来推出一个新的页面,不能跟web一样,直接丢一个链接地址就跳转到新的页面.当然,可以自己去加一个中间层来实现这些功能. Flutter里面是原生支持路由的.Flutter的framework提供了路由跳转的实现.我们可以直接使用这些功能. Flutter路由介绍 Flutte

  • C# WebApi 路由机制剖析

    前言:从MVC到WebApi,路由机制一直是伴随着这些技术的一个重要组成部分. 它可以很简单:如果你仅仅只需要会用一些简单的路由,如/Home/Index,那么你只需要配置一个默认路由就能简单搞定: 它可以很神秘:你的url可以千变万化,看到一些看似"无厘头"的url,感觉很难理解它如何找到匹配的action,例如/api/user/1/detail,这样一个url可以让你纠结半天. 它可以很晦涩:当面试官提问"请简单分析下MVC路由机制的原理",你可能事先就准备好

  • Flutter路由之fluro的配置及跳转

    目录 1.pubspec.yaml导包,注意格式~ 2.新建路由类,改类是定义页面的路径,然后将页面handler和路径设置到路由中 3.新建router_handler.dart,处理参数和跳转页面 4.调用传参 5.接收数据 6.问题来了,因为fluro无法直接传中文的,这里就需要用到编码码解码了,也就是encode和decode 1.pubspec.yaml导包,注意格式~ dependencies: flutter: sdk: flutter fluro: ^1.6.3 2.新建路由类,

随机推荐