Further advancements -Flutter Navigator details

Whether IOS or Android, there is a corresponding page stack management. In Flutter, page switching is managed using Navigator. Note that the concept of page in Flutter corresponds to Route. That Scaffold is not a Scaffold. The Scaffold is just a Widget that helps us build the page UI. So what does Navigator do and how does it work?


Push related operations

Future push(Route route)

Open a new page from the Router

// All Router information is maintained as a stack through a list
final List<Route<dynamic>> _history = <Route<dynamic> > [];Open a new page based on the Router information
@optionalTypeArgs
Future<T> push<T extends Object>(Route<T> route) {
  // take _history.last, which is the current page Router
  final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
  route._navigator = this;
  // Handle Overlay content at the top of the page
  route.install(_currentOverlayEntry);
  // Add the current Router to the list
  _history.add(route);
  route.didPush();
  route.didChangeNext(null);
  if(oldRoute ! =null) {
    // A callback to the previous Router internally handles some animation state
    oldRoute.didChangeNext(route);
    // Call back the old Router to the new Router
    route.didChangePrevious(oldRoute);
  }
  _afterNavigation(route);
  // Return a Future object
  return route.popped;
}
Copy the code

In Flutter, we often open a page with navigator.of (context).push(Router). Flutter maintains a List of routes via List

> _history. The whole process of Flutter isto add new routes to the end of the List, perform related callbacks, and return a Future object. This Future object is usually used for callbacks when the page is closed. It is understandable that the push page is a time-consuming operation with a return value, so when will the Future have a return value? Definitely when the page pop is launched. Sure enough, the pop method calls bool didPop(T result), executes the future’s complete([FutureOr

value]) and returns the value.


static Future pushNamed(String routeName, {Object arguments,})

Open a new page with routeName

1And the Navigator. The dart// Open a page based on the route name
@optionalTypeArgs
static Future<T> pushNamed<T extends Object> (String routeName, {
  Object arguments,
 }) {
  	// The push method above is eventually called, so _routeNamed returns a Router based on routeName
    return push<T>(_routeNamed<T>(routeName, arguments: arguments));
}
Copy the code
2And the Navigator. Dart Route < T > _routeNamed < T > (String name, { @required Object arguments, bool allowNull = false{})// Generate a setting based on RouteName
  final RouteSettings settings = RouteSettings(
    name: name,
    isInitialRoute: _history.isEmpty,
    arguments: arguments,
  );
  // The widget here is the Navigator, which should be passed in when the Navigator initializes
  Route<T> route = widget.onGenerateRoute(settings);
  if (route == null && !allowNull) {
    route = widget.onUnknownRoute(settings);
  }
  return route;
}

Copy the code

PushNamed

(String routeName, {Object arguments,}) (2) The Router is generated by widget.onGenerateRoute(Settings). So the onGener here is definitely passed in when the Navigator is initialized, so when is the Navigator initialized or how is the Navigator nested in the app?

Looking back at the call information, you see that the Navigator is nested in WidgetsApp, and that WigetsApp passed the following methods into the Navigator during initialization.

// A Map that stores routing information in the App. This is usually customized during runApp()
final Map<String, WidgetBuilder> routes;
Route<dynamic> _onGenerateRoute(RouteSettings settings) {
  final String name = settings.name;
  finalWidgetBuilder pageContentBuilder = name == Navigator.defaultRouteName && widget.home ! =null
      ? (BuildContext context) => widget.home
    	// Find the corresponding Router in the routing table
      : widget.routes[name];
  if(widget.onGenerateRoute ! =null)
    return widget.onGenerateRoute(settings);
  return null;
}
Copy the code

So the pushNamed

(String routeName, {Object Arguments,}) method uses the routing information we defined in the MaterialApp to generate the Router Object and finally call the push method. The following ones related to Name are similar, so no additional analysis will be done.


Future pushReplacement<T extends Object, TO extends Object>(Route newRoute, { TO result })

Close the current page and open a new one

Future<T> pushReplacement<T extends Object, TO extends Object>(Route<T> newRoute, { TO result }) {
  final Route<dynamic> oldRoute = _history.last;
  // The index is the removed routerIndex
  final int index = _history.length - 1;
  newRoute._navigator = this;
  newRoute.install(_currentOverlayEntry);
  _history[index] = newRoute;
  newRoute.didPush().whenCompleteOrCancel(() {
    // The old route's exit is not animated. We're assuming that the
    // new route completely obscures the old one.
    if (mounted) {
      // Call the previous page close and resource releaseoldRoute .. didComplete(result ?? oldRoute.currentResult) .. dispose(); }}); newRoute.didChangeNext(null);
  // State callback consistent with push
  if (index > 0) {
    _history[index - 1].didChangeNext(newRoute);
    newRoute.didChangePrevious(_history[index - 1]);
  }
  _afterNavigation(newRoute);
  return newRoute.popped;
}
Copy the code

By comparing the push process, pushReplacement

(Route

newRoute, {TO result}) adds the new Router TO the collection first. After the page Push is completed, the page before the Push will be cleaned up. If the result passed in the method parameter is not empty, it can be notified to the page opened on the previous page, and then the related state callback will be carried out just like Push.


Future pushAndRemoveUntil(Route newRoute, RoutePredicate predicate)

The predicate passes all the previous routers back to the caller, starting with the current page, closing the series of pages that satisfy the predicate, and then opening a new page.

@optionalTypeArgs
Future<T> pushAndRemoveUntil<T extends Object>(Route<T> newRoute, RoutePredicate predicate) {
  // The route that is being pushed on top of
  final Route<dynamic> precedingRoute = _history.isNotEmpty ? _history.last : null;
  // Routes to remove
  final List<Route<dynamic>> removedRoutes = <Route<dynamic> > [];// Each time the Router at the top of the stack is passed back to the outside world. If the predicate returns false, the Router is added to the to-be disabled list
  while(_history.isNotEmpty && ! predicate(_history.last)) {final Route<dynamic> removedRoute = _history.removeLast();
    removedRoutes.add(removedRoute);
  }

  final Route<dynamic> newPrecedingRoute = _history.isNotEmpty ? _history.last : null;
  newRoute._navigator = this;
  newRoute.install(precedingRouteOverlay);
  _history.add(newRoute);

  newRoute.didPush().whenCompleteOrCancel(() {
    if (mounted) {
     	// When the new page is successfully started, close all the pages collected above
      for (Route<dynamic> removedRoute in removedRoutes) {
        for (NavigatorObserver observer in widget.observers)
          observer.didRemove(removedRoute, newPrecedingRoute);
        removedRoute.dispose();
      }

      if(newPrecedingRoute ! =null) newPrecedingRoute.didChangeNext(newRoute); }}); newRoute.didChangeNext(null);
  for (NavigatorObserver observer in widget.observers)
    observer.didPush(newRoute, precedingRoute);
  _afterNavigation(newRoute);
  return newRoute.popped;
}
Copy the code

This method loops through the page at the top of the stack, judging by the predicate whether to destroy the page or not, until the predicate returns and stops, closing multiple pages from the current page. This is similar to android setting a page as a SingleTask


Pop operation

bool pop([ T result ])


@optionalTypeArgs
bool pop<T extends Object>([ T result ]) {
  final Route<dynamic> route = _history.last;
  bool debugPredictedWouldPop;
  // In didPop, result is passed to where the page is opened
  if (route.didPop(result ?? route.currentResult)) {
    if (_history.length > 1) {
      _history.removeLast();
      // If route._navigator is null, the route called finalizeRoute from
      // didPop, which means the route has already been disposed and doesn't
      // need to be added to _poppedRoutes for later disposal.
      if(route._navigator ! =null)
        // There is a set of _poppedRoutes to collect the exit page
        _poppedRoutes.add(route);
      	// The callback notifies the current page
      _history.last.didPopNext(route);
      for (NavigatorObserver observer in widget.observers)
        observer.didPop(route, _history.last);
    } else {
      return false;
    }
  }
  _afterNavigation<dynamic>(route);
  return true;
}
Copy the code

Pop is straightforward, closing the current page at the top of the stack and passing result back to where the page was started.


The Remove operation

void removeRoute(Route route)

Removes the specified Route

void removeRoute(Route<dynamic> route) {
  final int index = _history.indexOf(route);
  final Route<dynamic> previousRoute = index > 0 ? _history[index - 1] : null;
  final Route<dynamic> nextRoute = (index + 1 < _history.length) ? _history[index + 1] : null; _history.removeAt(index); previousRoute? .didChangeNext(nextRoute); nextRoute? .didChangePrevious(previousRoute);for (NavigatorObserver observer in widget.observers)
    observer.didRemove(route, previousRoute);
  route.dispose();
  _afterNavigation<dynamic>(nextRoute);
}
Copy the code

Remove the specified route from the stack and notify the pages before and after the Rout.