preface

This is an add-on to the blog Flutter Route-Navigator. It is not recommended to read this article if you have not read the main article, because it is really boring to Flutter chickens…

Without being polite, let’s explore the source code and some important details from Navigator’s push and pop methods.

push

When we want to push a Page on the interface, we can call the following code:

Navigator.push(
  context,
  PageRouteBuilder(pageBuilder: (context, animation, secondaryAnimation) {
    return MyPage(args);
  }));
Copy the code

We start with the Navigator. Push (BuildContext context, Route

Route) method:

static Future<T> push<T extends Object>(BuildContext context, Route<T> route) {
    return Navigator.of(context).push(route);
}
Copy the code

1.NavigatorStateObject to retrieve

As mentioned in the previous article, navigator. push is a static method that allows you to call it from anywhere, inside which the of method searches up the Element tree (BuildContext is Element’s abstract class). Let’s look at the navigator. of method:

static NavigatorState of(
    BuildContext context, {
      bool rootNavigator = false,
      bool nullOk = false,
    }) {
    final NavigatorState navigator = rootNavigator
        ? context.rootAncestorStateOfType(const TypeMatcher<NavigatorState>())
        : context.ancestorStateOfType(const TypeMatcher<NavigatorState>());
    return navigator;
}
Copy the code

The rootNavigator variable is used to determine whether to retrieve the rootNavigator, rootAncestorStateOfType looks up the root matching type object, and ancestorStateOfType looks up the nearest matching type object. The Navigator is a StatefulWidget, and the specific logic is contained in its State object.

2. Push implementation

Navigatorstate.push implementation:

//0
final List<Route<dynamic>> _history = <Route<dynamic>>[];

Future<T> push<T extends Object>(Route<T> route) {
   	...
   	 //1
    final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null;
    //2
    route._navigator = this;
    //3
    route.install(_currentOverlayEntry);
    //4
    _history.add(route);
    //5
    route.didPush();
    route.didChangeNext(null);
    if(oldRoute ! = null) { oldRoute.didChangeNext(route); route.didChangePrevious(oldRoute); } / / 6for (NavigatorObserver observer in widget.observers)
      observer.didPush(route, oldRoute);
    _afterNavigation();
    return route.popped;
}
Copy the code
  • 0._history is the interface stack maintained by the Navigator, but it’s just a plain List.

    1. Get the route that was at the top of the stack because_historyIt’s an ordinary oneList, so the top of the stack is the last element.
  • 2. Bind the newly added route to the Navigator reference.

  • 3. Install is an important process for converting route to OverlayEntry and inserting it into List

    . _currentOverlayEntry is the OverlayEntry corresponding to oldRoute. Passing _currentOverlayEntry means inserting into it. We’ll get into the details later.

  • 4. The route into the stack.

  • 5. Completed the conversion of the old and new interfaces, with some internal events and animation processing.

  • 6. Notify all Navigator observers.

3. route.install

We are most concerned about step 3, route.install(_currentOverlayEntry); This method is an empty implementation of the Route class. In its subclasses, we will focus on the implementation of OverlayRoute:

abstract class OverlayRoute<T> extends Route<T> {
...
  @override
  List<OverlayEntry> get overlayEntries => _overlayEntries;
  final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];
  
  @override
  void install(OverlayEntry insertionPoint) {
    _overlayEntries.addAll(createOverlayEntries());
    navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint);
    super.install(insertionPoint);
  }
...
Copy the code

_overlayEntries for a Route usually contain two overlayentries, a mask and the interface itself, created in createOverlayEntries.

Add two overlayentries to _overlayEntries, then call the Overlay object held by the Navigator and insert the mask and interface into the List

held by the overlay for drawing onto the interface.

4. createOverlayEntries

This is the key to Route to OverlayEntry, which is implemented in ModalRoute:

abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> { ... @override Iterable<OverlayEntry> createOverlayEntries() sync* { yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier); yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState); }...Copy the code

It doesn’t matter if you’re unfamiliar with the yield syntax, just know that this method will eventually return two OverlayEntry objects. _modalBarrier is the mask layer, the Dialog background mask.

We focus on the creation of the second page. OverlayEntry is passed two parameters:

  • Builder: This is a function, and our custom Page creation is in this method.

  • MaintainState: This property, discussed in the previous blog, indicates whether the Widget is needed to maintain its state when it is not visible and whether it is needed to be kept alive. Usually a Page maintainState is true and Dialog is false.

5. _buildModalScope

Take a look at the _buildModalScope implementation:

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

_buildModalScope creates Widget _ModalScope and passes Route in itself. What is _ModalScope?

_ModalScope is a StatefulWidget, so let’s look directly at its State build method:

@override
  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: widget.route.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(// ======!! Focus on this code !!!!!! builder: (BuildContext context) {returnwidget.route.buildPage( context, widget.route.animation, widget.route.secondaryAnimation, ); .Copy the code

You can see that _ModalScope has a lot of widgets nested inside it, and the data used to create them is from Route. We’ll focus on the last Builder, and you can see that its last return calls Route’s buildPage method. Is the custom PageRouteBuilder object we passed in when we first called push:

Navigator.push(
  context,
  PageRouteBuilder(pageBuilder: (context, animation, secondaryAnimation) {
    return MyPage(args);
  }));
Copy the code

Here you know how your incoming Route will be used.

Let’s go back and look at how Overlay works after Route is converted to OverlayEntry:

6. Insert into the Overlay

We inserted it into the overlay like this :navigator.overlay? .insertAll(_overlayEntries, above: insertionPoint); Navigator. overlay is actually OverlayState

class OverlayState extends State<Overlay> with TickerProviderStateMixin { ... void insertAll(Iterable<OverlayEntry> entries, { OverlayEntry above }) { ... / / 1for (OverlayEntry entry inentries) { entry._overlay = this; } / / 2setState(() { final int index = above == null ? _entries.length : _entries.indexOf(above) + 1; _entries.insertAll(index, entries); }); }...Copy the code
    1. Insert each one into the collectionOverlayEntryBind itself, bind itself for the reason we talked about in the last article:Element of autonomy. Insert is made ofOverlay, but deletion is invoked by each element itself.
    1. I saw something familiarsetState((){}Method that is fired after the collection has been insertedOverlayRebuild.

So let’s look at the build method of OverlayState:

@override
  Widget build(BuildContext context) {
    //1
    final List<Widget> onstageChildren = <Widget>[];
    final List<Widget> offstageChildren = <Widget>[];
    bool onstage = true; / / 2for(int i = _entries.length - 1; i >= 0; i -= 1) { final OverlayEntry entry = _entries[i]; / / 3if (onstage) {
        //4
        onstageChildren.add(_OverlayEntry(entry));
        if (entry.opaque)
          onstage = false; / / 5}else if (entry.maintainState) {
        offstageChildren.add(TickerMode(enabled: false, child: _OverlayEntry(entry))); }} / / 6return _Theatre(
    	//7
      onstage: Stack(
        fit: StackFit.expand,
        children: onstageChildren.reversed.toList(growable: false),
      ),
      //8
      offstage: offstageChildren,
    );
  }
Copy the code
  • 1. Create two empty lists, one for “on-stage” widgets that will be drawn, and one for “off-stage” widgets that don’t need to be drawn.

  • 2. Start walking through all the overlayentries, ready to assign them to two sets. Note that the loop is traversed in reverse order, drawing the last element to the top.

  • 3. At the beginning, each OverlayEntry had a chance to be drawn until opaque=true of one OverlayEntry, and the other OverlayEntry did not have a chance to “come on stage”

  • 4. You can see that OverlayEntry is passed as a parameter to _OverlayEntry, completing the conversion from a pure Dart class to a Widget. The _OverlayEntry code is simple and builds based on the properties in OverlayEntry.

  • 5. OverlayEntry that has no chance to get on stage starts to determine maintainState values. The maintainState values that need to be saved enter Stage Hildren, and those that do not need to be saved do not have the chance to participate in this build, they will be destroyed.

  • 6. After the allocation, enter the theatre: _Theatre.

  • 7. Switch to the Stack component to be drawn.

  • 8. Do not need to be drawn, only build.

At this point, the Page for the new push is created and drawn.

pop

Having looked at push, let’s look at Pop:

1.pop

bool pop<T extends Object>([ T result ]) { ... //1 final Route<dynamic> route = _history.last; bool debugPredictedWouldPop; / / 2if (route.didPop(result ?? route.currentResult)) {
      if (_history.length > 1) {
       //3
        _history.removeLast();
        if(route._navigator ! = null) _poppedRoutes.add(route); //4 _history.last.didPopNext(route); / / 5for (NavigatorObserver observer in widget.observers)
          observer.didPop(route, _history.last);
      } else {
        return false; }}else{... } _afterNavigation();return true;
  }
Copy the code
  • 1. Get the route at the end of the collection, the top of the stack, which will be popped.

  • 2. You can see that didPop has a return value, which means that if false is returned, it does not pop. If true is returned, there’s some destruction inside didPop, which we’ll look at later.

  • 3. If didPop returns true, stack processing is done.

  • 4. Notify the next route that you’re back in the foreground.

  • 5. Inform all observers.

2.didPop

We focus on Page collection handling, so take a look at didPop in OverlayRoute:

abstract class OverlayRoute<T> extends Route<T> {
...
  @override
  bool didPop(T result) {
    final bool returnValue = super.didPop(result);
    if (finishedWhenPopped)
      navigator.finalizeRoute(this);
    return returnValue;
  }
Copy the code

Have a look at the navigator. FinalizeRoute

void finalizeRoute(Route<dynamic> route) {
    ...
    route.dispose();
  }
Copy the code

left

abstract class OverlayRoute<T> extends Route<T> {
...
@override
  void dispose() {
    for (OverlayEntry entry in _overlayEntries)
      entry.remove();
    _overlayEntries.clear();
    super.dispose();
}
Copy the code

You can see that iterating through all the remove methods of OverlayEntry executes:

void remove() { //1 final OverlayState overlay = _overlay; _overlay = null; / / 2if (SchedulerBinding.instance.schedulerPhase == SchedulerPhase.persistentCallbacks) {
      SchedulerBinding.instance.addPostFrameCallback((Duration duration) {
        //3
        overlay._remove(this);
      });
    } else{ overlay._remove(this); }}Copy the code
  • 1. Clear references to avoid memory leaks

  • 2. Check the current scheduler status and run the _remove method of OverlayState later or immediately.

void _remove(OverlayEntry entry) {
    if (mounted) {
      _entries.remove(entry);
      setState(() { /* entry was removed */ }); }}Copy the code

Get the current OverlayEntry from the collection and trigger a rebuild of the Overlay, since _entries no longer have the current interface and the rebuild will naturally cease to exist.

conclusion

Ok, the push and pop code flow is done