Refer to code version 1.18

The entire flutter application is based on a view in a native application, such as Android’s FlutterView. Page switching in Flutter relies on its routing mechanism, which is a set of functions centered around Navigator. Enables it to perform similar and customizable page switching effects as native.

This section describes the implementation principle of route in FLUTTER, including the initial page loading and the underlying mechanism of page switching.

Realize the basic

The application of Flutter relies on the MaterialApp/CupertinoApp widgets. These widgets correspond to the Android /ios design style respectively. They also provide some basic facilities for the application to run, such as the main page and routing table related to routing, etc. For example, theme, locale, and so on are related to the overall page display.

Routing configurations include home, routes, initialRoute, onGenerateRoute, and onUnknownRoute. These correspond to home page widgets, routing tables (finding widgets based on routes), routes at first load, route generators, and unknown routing proxies (such as common 404 pages).

The children of the MaterialApp/CupertinoApp are WidgetsApp, but they pass different parameters to WidgetsApp, which makes the interface styles of the two widgets different. The Navigator is created in the 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) {
          returnwidget.onGenerateInitialRoutes(initialRouteName); }, onUnknownRoute: _onUnknownRoute, observers: widget.navigatorObservers, ); }... }Copy the code

The first thing created in the WidgetsApp build is the Navigator. Just look at its parameters. First, _navigator is a GlobalKey. This enables WidgetsApp to use the key to call the Navigator’s function for route switching, that is, to process the native route switching information in a WidgetsBinding, which is ultimately completed by WidgetsApp. In addition, the _navigator should only be used in WidgetsApp. You can use it anywhere else by calling navigator. of, which will look up the Element tree to NavigatorState. So switching routes in the application needs to be wrapped in Navigator, but since Navigator is generated in WidgetsApp, you don’t have to worry about this in development.

There are two ways to get an upper NavigatorElement instance from the lower layer in the Element tree. One is to use an InheritedWidget. Ancestorxx of ExactType series. The principle of the two methods is basically the same, except that InheritedWidget is passed down layer by layer while building the tree, while the InheritedWidget looks up when used. So using an InheritedWidget is more efficient in this sense, but InheritedWidget has more advantages than that. It notifies all nodes that depend on it to update when data changes. That’s something ancestorxx exactType doesn’t have.

Then initialRoute specifies initialization time page, by WidgetsBinding. The instance. The window. The defaultRouteName and widget initialRoute to decide, but a higher priority, Since this is specified in Native, taking Android as an example, the route field can be passed to specify the initialization page when FlutterActivity is started.

OnGenerateRoute and onUnknownRoute are strategies for obtaining routes. When onGenerateRoute fails to hit, onUnknownRoute will be called to give a default page. OnGenerateInitialRoutes used in the production of the routing list when start the application, it has a default implementation defaultGenerateInitialRoutes, according to the passed initialRouteName choose different Route, If the incoming initialRouteName routing the Navigator is not the default home page. The defaultRouteName, flutter would not initRoute as the main page, Instead, the default route is pushed and then the page corresponding to initRoute is pushed, so if popRoute is called after that, it will return to the main page

Observers are a list of listeners for route switching, which can be passed in externally to do something about it, such as HeroController, which is a listener.

The Navigator is a StatefulWidget that completes the process of converting initRoute to Route in initState of NavigatorState and pushes it into push to generate OverlayEntry. And that’s going to pass down to the Overlay that’s going to display the page.

During the push process, the route is converted into a list of Overlayentries, and each OverlayEntry stores a WidgetBuilder. In a way, OverlayEntry can be considered as a page. All pages are coordinated and displayed using Overlay. Overlay is a Stack structure that displays multiple children. In its initState,

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

InitialEntries are stored in _entries.

Overlay is a control that determines the display page based on the route. It is simple to implement:

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,
  );
}
Copy the code

In the build function, all OverlayEntry is divided into visible and invisible parts. Each OverlayEntry generates an _OverlayEntry, a StatefulWidget that controls the redrawing of the current page. In _Theatre, visible and invisible children are converted to elements, but when drawing, _Theatre’s corresponding _RenderTheatre will only draw visible children.

The opaque variable is used to judge whether an OverlayEntry can completely block another OverlayEntry. The opaque variable is given by Route. When the page animation is executed, this value will be set to false. Then, after the page switching animation is completed, the Opaque parameter of Route is assigned to its OverlayEntry. In general, the Route corresponding to the window is false and the Route corresponding to the page is true.

So, after a page switch, the previous page will always be in the Element tree, but it will not be drawn in the RenderObject. This is also reflected in the Flutter Outline tool. In this sense, the more pages there are in a flutter, the more steps there are to process. Although there is no need to draw the bottom page, the basic traversal of the whole tree is still required, which is also an overhead.

_routeNamed

Page management in Flutter mainly relies on the Route management system. The entry point of flutter is the Navigator. What it manages is essentially the Route carrying the user’s page. We can pass in the routing table directly to the MaterialApp/CupertinoApp. Each name corresponds to a WidgetBuilder. Then combine the pageRouteBuilder (this can be customized, but the MaterialApp/CupertinoApp has a default implementation that converts WidgetBuilder to Route), RouteName can be converted to 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;
}
Copy the code

This is a three-step process that generates RouteSettings, calls onGenerateRoute to get a route from the routing table, and if it doesn’t hit, calls onUnknownRoute to give something like a 404 page.

OnGenerateRoute and onUnknownRoute are passed in when you build Navigator, implemented in WidgetsApp,

Route<dynamic> _onGenerateRoute(RouteSettings settings) {
  final String name = settings.name;
  finalWidgetBuilder 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;
}
Copy the code

If it is the default route, it will directly use the given home page (if there is one), otherwise it will directly check the routing table. Therefore, in essence, the home page here is more a symbol of identity, and it doesn’t matter if there is no one. In addition, the main output of the routing table is WidgetBuilder, which needs to be packaged once to become a Route. Or if you don’t want to use the routing table, you can also directly implement onGenerateRoute functions to generate routes directly according to the RouteSetting. This is not just a matter of returning WidgetBuilder, you need to wrap it yourself.

OnUnknownRoute provides a page similar to 404, which also returns the Route directly.

_flushHistoryUpdates

From which version, the route management of flutter introduces states. Instead of implementing each push and pop individually, all route switching operations are represented by states, and all routes are encapsulated as _RouteEntry. It has an internal implementation of the Route operation, but it is divided into smaller units and is state dependent.

Status is an enumeration with a progressive relationship. Each _RouteEntry has a variable that stores the current state. In _flushHistoryUpdates, all _routeentries are iterated and processed according to their current state. Once all the routes are processed together, the process becomes clearer and can be reused to a large extent, for example, push and pushReplacement. This used to be done separately in the two methods, but now they can be done together. The only difference is that the latter has one more remove operation than the former.

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 StringrouteName = 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); }Copy the code

In addition to status processing, the whole process of _flushHistoryUpdates will be iterated through the entire route list, and different processing will be done according to the status. However, it can only process one or two of the top layer, and the rest will be skipped directly. After processing, call _flushRouteAnnouncement to link routes before and after, such as animation linkage, etc.

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; }}Copy the code

And the implementation is a little bit clearer, so for each _RouteEntry, we call didChangeNext and didChangePrevious to establish a connection, For example, bind the secondaryAnimation of the current Route to the animation of the next Route in didChangeNext to animate it, or get the title of the previous Route in didChangePrevious, This is the title of the previous page in the CupertinoNavigationBar.

MaybeNotifyRouteChange is then called to issue a notification specifying the Route currently in the display state.

Finally, toBeDisposed executes the destruction of _RouteEntry, and the list will store the routeentry that needs to be removed during the loop above, By calling the OverlayEntry remove function, which removes itself from the Overlay, and the OverlayEntry Dispose function, which calls the Route’s dispose function, Such as AnimationController destruction in TransitionRoute).

Finally, with regard to states, here are all states:

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
}
Copy the code

Essentially, these states fall into three categories: Add (added directly upon initialization), Push (similar to Add, but with animation), POP (removed from a page), and Remove (removed from a page, with no animation or location restrictions relative to POP).

add

Adding a route is currently only used to add an initialization page during application initialization, corresponding to initState of NavigatorState.

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();
}
Copy the code

It adds all the initial routes from onGenerateInitialRoutes to _RouteEntry to _history, where their status is _routelifecycle.add, Then we call _flushHistoryUpdates to process it.

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;
  }
  // ...
}
Copy the code

The Add route basically calls two functions, handleAdd and 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;
}
Copy the code

The install function can be thought of as a Route initialization function, such as creating a ProxyAnimation in ModalRoute to manage the execution of some animations. The AnimationController for performing the switch animation is created in TransitionRoute, and the OverlayEntry for the current Route is created and inserted in OverlayRoute. CreateOverlayEntries is used to create OverlayEntry. ModalRoute

可迭代<OverlayEntry> createOverlayEntries() sync* {
  yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier);
  yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState);
}
Copy the code

Each Route generates two overlayEntries, _buildModalBarrier, which creates a barrier between two pages that we can use to set a background color for new pages and animate transitions, and _buildModalScope, It generates the actual content of the page, with multiple layers of wrapping around it, the bottom of which is the widget created by WidgetBuilder.

So if I look at the implementation of these two functions,

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,
  );
}
Copy the code

ModalBarrier is a barrier between two routes. It can be configured to indicate the separation of two routes by color, interception events, etc. IgnorePointer is used to indicate that there is no response time when switching animations are performed.

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) {
                returnwidget.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) {
                    returnwidget.route.buildPage( context, widget.route.animation, widget.route.secondaryAnimation, ); }, ((), ((), ((), ((), ((), ((); }Copy the code

_ModalScope needs to host the presentation of the user interface, and its build function can see that there are many layers on top of widget.route.buildPage to create a user-defined page.

  1. _ModalScopeStatus, which InheritedWidget is used to provide data to the underlying node
  2. Offstage, which can be controlled by the Offstage variable
  3. PageStorage, which provides a storage policy called PageStorageBucket. This class can bind specific data to a BuildContext, support writes and reads, store state for a widget, etc
  4. FocusScope is used for focus management. Generally, only the control that obtains focus can receive key information
  5. RepaintBoundary controls the scope of repainting, intended to reduce unnecessary repainting
  6. AnimatedBuilder, the animation control Widget, will rebuild from the Animation
  7. Widget. The route. BuildTransitions, it can have different implementations in different route, such as the Android’s default implementation is from bottom to enter, the default implementation of ios is sliding from right to left, It is also possible to customize the switch animation by customizing Route or ThemeData. Another point needs to be noted: The animation in Route is divided into animation and secondaryAnimation. SecondaryAnimation defines the animation when the new page is pushed. For example, in ios style, the new page slides from right to left, and the previous page slides as well. The animation that slides the previous page is secondaryAnimation
  8. IgnorePointer, also used in page switch animation execution, disallows user action
  9. RepaintBoundary, the consideration here should be to consider that there is an animation at the top, so it should be wrapped up to avoid redrawing the fixed content
  10. The only function of Builder should be to provide BuildContext. Although every build function has a BuildContext parameter, this is for the current Widget, not its immediate parent, which can be a bit abstract. For example, buildPage needs to use BuildContext as an argument, so if it needs context’s ancestorStateOfType, it’s actually looking up from _ModalScopeState, Instead of starting with Builder and looking up
  11. Widget.route. buildPage, which uses the WidgetBuilder of Route to build the user interface inside, may also be wrapped again for different routes

So that’s the layout nesting from Overlay down in a page. Once the new OverlayEntry is created, it passes them to the Overlay. In the process, the setState function of the Overlay is called to request redrawing, and the Overlay is switched between old and new pages.

This is the entire process of install. When you’re done, change currentState to Adding and return.

One thing to note here is that the while loop iterates through all _routeentries from top to bottom, but it does not execute the next _RouteEntry until a continuous operation has completed. This is implemented by the continue keyword in the code, This keyword returns the next loop, but it does not update the current _RouteEntry, so it is still the same route. This is usually used when the _RouteEntry status changes and needs to be processed continuously, so for add, Immediately after it’s done, it does the adding block, which is 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);
}
Copy the code

Route’s didAdd function indicates that the Route has been added and does some finishing, such as updating the AnimationController value to maximum in TransitionRoute, setting transparency, and so on. DidAdd then sets the state to Idle and calls didPush for all listeners. Idle indicates that an _RouteEntry has been processed. Subsequent operations such as POP and replace need to be processed again. The add process can also be finished here.

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;
}
Copy the code

Push encapsulates Route as _RouteEntry into _history and calls _flushHistoryUpdates, whose initial state is push, and returns Route.popped at the end, which is a Future object, Can be used when the previous page receives the return result of the new page. This value is passed when the pop is currently routed.

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;
  }
  // ...
}
Copy the code

Push, pushReplace, and replace are all grouped together, and it calls handlePush first, which contains handleAdd and didAdd in the add process. For example, when you call Install and didPush, push/pushReplace has a transition that starts with an animation and changes its state to pushing, At the end of the animation, it cuts to idle and calls _flushHistoryUpdates. Replace calls didReplace to complete the page replacement. Again, call the notification function.

pop

The pop process is not quite the same as the above two, it also has some operations in 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);
}
Copy the code

The pop that calls _RouteEntry, in which case it calls Route’s didPop, passing the return value, moving out the animation to start, and so on. But in OverlayRoute:

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

The call to finalizeRoute relies on the value of finishedWhenPopped, which can be modified in subclasses, such as TransitionRoute, which is false and easy to understand. The Route cannot be destroyed directly after didPop is executed in TransitionRoute. Instead, the TransitionRoute animation should be executed first, and if the animation is not required, it can be called directly, otherwise it can be executed after the animation is completed. This is achieved by listening for the animation state. In 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; } ()); }Copy the code

In finalizeRoute, it will judge whether it is in the process of POP. If it is, it means that finalizeRoute is called directly at this moment. Then it needs to execute the operation of POP state first and then dispose to switch the state to dispose. If not, it means that this function is called when the animation is finished, so the pop state processing is completed, so the pop processing step is skipped, as shown above. Let’s take a look at what the POP process does.

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;
  }
  // ...
}
Copy the code

HandlePop switches the state to poping and then issues a notification. The poping state is not processed because it is an interim state that automatically switches to dispose after the execution of the animation. Similarly, the pushing state above, In the Dispose branch, you remove _RouteEntry from _history and add it to toBeDisposed, and then dispose it collectively after the traversal.

remove

The logic of remove isto find a _RouteEntry from _history that matches the one passed in, set its state to remvoe, and call _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;
  }
  // ...
}
Copy the code

HandleRemoval will be called, notification will be called, state will be switched to removing, then dispose will be disposed, then toBeDisposed, so there’s no animation involved, Pop is generally used only to remove pages that are not being displayed, otherwise pop is recommended.

conclusion

Above is the routing mechanism for the implementation of the principle, in terms of its overall, most refreshing that gives a person is the addition of state management, through divide the in and out of a page to a different state, is can effectively reduce the complexity of the code, but from the point of the present results, this a process execution is not enough refining, such as the division of state is not reasonable, From the design of these states, add/push/ POP all have the corresponding ING form to indicate that they are being executed, but I don’t see the necessity of adding for the moment, and I feel there are some problems in the organization of the code. For example, handleAdd and handPush actually have a large part of the same code, which will not be optimized in the future.

The _routeNamed function is not open to the public, and not all routing operations, such as removeRoute, provide a wrapper with a name as an input parameter, which makes it difficult to call.