Past wonderful

πŸ‘‰ Flutter will know will series – start Overlay of Navigator

πŸ‘‰ Flutter will know will series — Unsung Heroes _Theatre

Overlay components are used to Overlay components. This component stacking effect is the page stacking effect of our APP, which can be opened page by page. In addition, we also introduced the unsung hero behind Overlay _Theatre. By using _Theatre, we can simply draw the content standing on the stage. We got closer and closer to the truth of Overlay page management.

This article, we introduce a front β€”β€”β€”β€” Route. Route appears in various places in our code, such as the page we open, in the MaterialRoute, our popover in the RawDialogRoute, and our bottom half Sheet in the _ModalBottomSheetRoute. Let’s take a look at Route, and you’ll have a new look at the page.

RouteWhat is the

Let’s look at the official definition of Route.

An abstraction forAn entry managed by a [Navigator] abstract entity Thisclass defines an abstract interface between the navigator and the routes
that are pushed on and popped off the navigator. Most routes havevisual
affordances.which they place in the navigators Overlay using oneor more
OverlayEntry objectsThis class defines some abstract interfaces that can be used tonavigator 去 poporpush. Most of therouteIt's all visual becauserouteWill be placed inOverlayIn the.Copy the code

From the description above, Route is an abstract interface class managed by the Navigator. This class defines some abstract interfaces that the Navigator can pop or push.

Because of overlays, most routes are visible.

The description doesn’t seem like much, but we look at the interfaces it encapsulates: member variables and member methods.

Member variables

Member variables are the flesh and blood of a class. As interface Route, there are only three member variables.

The property name type role
_settings RouteSettings Route related Settings, such as names, parameters, etc
overlayEntries List Displays the OverlayEntry of the page
navigator NavigatorState The Navigator that hosts the Route
restorationScopeId ValueListenable Id of page data storage and recovery

These variables are initialized at class declaration time, so they live and die with the Route page. The Navigator is a StatefulWidget that has no ability to draw itself, and its build method constructs subtrees called overlays.

When Route is pushed in, overlayEntries are added to the Overlay, so Route’s are displayed on the page. See the previous section on overlays for πŸ‘‰ Overlay guide to the start of the Flutter Overlay series – Navigator. So at this point, we know that our pages, our dialogs, are all going to be displayed in the Overlay.

Because Navigator is a StatefulWidget, its State is NavigatorState, which is a reference to Navigator, so Route holds NavigatorState, You can sense the NavigatorState declaration cycle.

RouteSettings is a routing configuration that defines names and parameters for routes. For example, when we want to open a page by name, we construct a Route through the RouteSettings, in the same way that we close the page stack to a Route by name. , etc.

These three attributes are the most important attributes of Route, and restorationScopeId is a new attribute that is responsible for page data recovery, which we will discuss later.

Let’s look at the interface methods defined.

Members of the method

Member variables are the flesh and blood of a class, so member methods are the skeleton of a class, defining the behavior of a Route. The Navigator implements administrative functions by calling these interfaces: initialize, open, close, change, judge, and so on. Here we introduce them one by one:

Initialize install

This method is the initialization method of the Route, which is called first when the Route is inserted into the Navigator (push).

OverlayEntries of Route are usually added to the Overlay, and different subclasses add their own content, such as animation effects, etc.

DidPush into the page

@protected @mustCallSuper TickerFuture didPush() { return TickerFuture.complete().. then<void>((void _) { if (navigator? .widget.requestFocus == true) { navigator! .focusScopeNode.requestFocus(); }}); }Copy the code

After Route is pushed, install is called to initialize, and then this method is called. The default behavior is to regain focus after the Route content is displayed on the page.

Add didAdd to the page

The default processing behavior is the same as didPush, and the timing of the call is also after Install. The only difference is that the didAdd scenario is a Route that does not require transitions, and the Route needs to be on the page immediately. For example, when the page data is restored, the page is displayed immediately, and the reconstructed Route is didAdd.

Switch pages to didReplace

The default is not implemented, and the call time is after install. The difference is that Route appears as a switch.

Whether to turn off the page’s willPop

This is where the Navigator’s maybePop is called to determine whether to close the Route.

Normally, Pop is not called when there is only one Route left in the stack, because it will appear black.

Launch the page for didPop

This method is called when a Pop Route request is issued.

The return value of this method is critical because it guarantees that the animation will not be executed. If the method returns true, the Navigator removes the route from the history list, but Dispose does not call.

DidComplete for Route completion

Computes the asynchronous result of pop

Become top level Route didPopNext

The nextRoute parameter is popped and the current Route becomes the topmost Route.

Route hierarchy changes in didChangeNext

Specify the nextRoute as nextRoute. This method is called whenever the next route of the route changes.

Route hierarchy change didChangePrevious

The Route preceding the Route is specified as previousRoute. This method is called whenever the Route preceding the Route changes.

The changedInternalState of the internal state change

This method is called when the internal state of the Route changes.

This method is called on exchanges like Willhandlepopexchanges, didPop, offstage and other internal values.

For example, ModalRoute uses this method to notify child nodes that information has changed.

ChangedExternalState of external state changes

When Navigator is rebuilt, that indicates that Route may need to be rebuilt as well. For example, when the MaterialApp is rebuilt.

This ensures that the Route relies on widgets that build the MaterialApp to be notified when state occurs.

Dispose released by Route

The Route will remove overlays and release other resources that no longer hold references to the Navigator

Route’s network

We now know that Route is an abstract class that Navagator uses to manage pages. An array of overlayEntry that is held internally and inserted into the Overlay. And defines a lot of abstract methods, the design of these methods is object-oriented design, as a Route to have these capabilities.

Let’s look at Route’s network.

Above is the system network of Route. Route and its subclasses hold multiple overlayentries, and each OverlayEntry will be displayed on the page. For example, the black mask of the popover is an OverlayEntry. The popover content displayed is again an OverlayEntry. Route also holds NavigatorState, which gives you access to NavigatorState properties and methods, such as focus, and so on.

In addition, routes are inherited, with each layer implementing specific functions. OverlayRoute implements the ability to insert OverlayEntry into an Overlay. TransitionRoute implements the animation switch function of Route, and we see that it holds the animation object and animation controller object. ModalRoute is relatively complete and does return interception, which is the usual WillPopScope component. Based on ModalRoute, there are two types: PopupRoute, popWindow type PopupRoute, PageRoute, Rout E is the most common Route in our development.

Let’s take a look at the respective functionality added by the different levels through initialization and release.

Route is linearly initialized

The Route initialization is the install method we talked about above. Let’s look at what is initialized and how each layer is initialized.

Route.install

Route is the most basic class that defines the specification.

void install() { }
Copy the code

As you can see, the method is declared in Route, but not implemented.

OverlayRoute.install

@override
void install() {
  assert(_overlayEntries.isEmpty);
  _overlayEntries.addAll(createOverlayEntries());
  super.install();
}

@factory 
可迭代<OverlayEntry> createOverlayEntries();

Copy the code

Step 1: OverlayRoute overlayEntries add all the overlayEntries of the Route, which may be one or more.

Note: createOverlayEntries are abstract methods, and non-abstract subclasses must tell the Framework what to display!

As we will see later, createOverlayEntries returns only those to be added or displayed.

TransitionRoute.install

@override
void install() {
  _controller = createAnimationController(); / / first place_animation = createAnimation() .. addStatusListener(_handleStatusChanged);/ / the third place
  super.install();/ / the first around
  if(_animation! .isCompleted && overlayEntries.isNotEmpty) { overlayEntries.first.opaque = opaque; } } AnimationController createAnimationController() {final Duration duration = transitionDuration;
  final Duration reverseDuration = reverseTransitionDuration;
  return AnimationController( / / in the second place
    duration: duration,
    reverseDuration: reverseDuration,
    debugLabel: debugLabel,
    vsync: navigator!,
  );
}
Copy the code

The first code creates the animation controller, which by default is an animation controller with animation duration transitionDuration

The default process created is createAnimationController, pay attention to in the second place code here.

We know that the animation needs a vsync parameter. Vsync is usually mixed with the TickerProviderStateMixin State, and NavigatorState is such a State, so vsync passes in navigator, This is why the Route holds a reference to the Navigator.

The third code creates an animation object, _animation, that drives the page transitions and adds a state listener to the animation.

Listen to:

void _handleStatusChanged(AnimationStatus status) {
  switch (status) {
    case AnimationStatus.completed:
      if (overlayEntries.isNotEmpty)
        overlayEntries.first.opaque = opaque;
      break;
    / /... Omit code}}Copy the code

For example, animation completion, where opaque of the first overlayEntry in the overlayEntries array is set to opaque of the constructor.

Remember what opaque property does? If opaque is true, the overwritten content will not be drawn. If opaque is true, the bottom page will not be drawn. You can watch πŸ‘‰ Flutter will know will series — Unsung Heroes _Theatre.

Last but not least, notice # 4: the super content of the TransitionRoute is initialized before the animation itself is initialized, i.e. the animation is initialized before the overlayEntry content.

Thus: the TransitionRoute creates the animation object (the current animation progress) and the animation controller.

ModalRoute.install

@override
void install() {
  super.install();
  _animationProxy = ProxyAnimation(super.animation);
  _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
}
Copy the code

ModalRoute generates two agent animations.

Proxy: Accepts an Animation class as its parent and forwards only the state of that parent. The value of _animationProxy is the value of animation.

Here’s why two animations are generated: the first one needs to exit and the second one needs to enter.

Animationproxy: animates the push and pop animations of the current Route, and animates the animations that drive the previous Route, such as the pop animations that drive the previous Route.

Secondaryanimationproxy: The animation of the Route placed on top of the Route, which connects the Route itself to the entry and exit animations of the new Route.

This is the install method, OverlayRoute. Install also has a createOverlayEntries abstract method. Let’s look at the implementation of ModalRoute.

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

A Dart syntax is used here: sync* and yield

Sync * and yield are a set of syntactic keywords for Dart. Sync * is placed on the method signature to indicate that the method returns an Iterable array object. The elements of the array are yield generated for each row.

So the array returned by the createOverlayEntries method contains two elements — modalBarrier and OverlayEntry.

MaintainState field: maintainState field, which indicates whether the Route status is required to be maintained. This is the Entry in the maintainState area described earlier.

We know that pages are superimposed, so if a page is not displayed and is covered by the top page, does it need to live in memory? That’s what this field means. If true, the Route is saved, and some Future results of the current Route, the previous Route overridden, can be computed normally, such as refreshing, etc. If set to false, it will be reclaimed when memory is tight.

The familiar MaterialPageRoute is true.

Now, let’s take a look at what was added.

Mask _modalBarrier

Widget _buildModalBarrier(BuildContext context) {
  Widget barrier;
  if(barrierColor ! =null&& barrierColor! .alpha ! =0 && !offstage) { // changedInternalState is called if barrierColor or offstage updates
    finalAnimation<Color? > color = animation! .drive( ColorTween( begin: barrierColor! .withOpacity(0.0),
        end: barrierColor, // changedInternalState is called if barrierColor updates
      ).chain(CurveTween(curve: barrierCurve)), // changedInternalState is called if barrierCurve updates
    );
    barrier = AnimatedModalBarrier( / / first place
      color: color,
      dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
      semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
      barrierSemanticsDismissible: semanticsDismissible,
    );
  } else {
    barrier = ModalBarrier( / / first place
      dismissible: barrierDismissible, // changedInternalState is called if barrierDismissible updates
      semanticsLabel: barrierLabel, // changedInternalState is called if barrierLabel updates
      barrierSemanticsDismissible: semanticsDismissible,
    );
  }
  / /... Omit code
  barrier = IgnorePointer(/ / in the second placeignoring: animation! .status == AnimationStatus.reverse ||// changedInternalState is called when animation.status updatesanimation! .status == AnimationStatus.dismissed,// dismissed is possible when doing a manual pop gesture
    child: barrier,
  );
  return barrier;
}

Copy the code

BarrierColor is the mask color we often see in dialog boxes.

Barrierdistransmissible is whether we click on a mask to disappear or not.

ModalBarrier, animated or not, is the core of the Route. ModalBarrier is the first one in the Route.

ModalBarrier does three things: add a mask background, click the mask to go back to the page, and prevent events from penetrating.

Ignoring true, the Barrier does not respond to the gesture. The dialog box is displayed and the black mask surrounding the barrier responds to the gesture.

The main purpose of adding this layer is to prevent gestures from passing to the next layer of the page.

The actual content _buildModalScope

Widget _buildModalScope(BuildContext context) {
  // To be sorted before the _modalBarrier.
  return_modalScopeCache ?? = Semantics( sortKey:const OrdinalSortKey(0.0),
    child: _ModalScope<T>(
      key: _scopeKey,
      route: this.// _ModalScope calls buildTransitions() and buildChild(), defined above)); }Copy the code

_buildModalScope builds _ModalScope and specifies a member variable key instead of a temporary variable key. This allows you to reuse elements, reducing repetitive builds. πŸ‘‰ Flutter must know must know series — Update reuse mechanism of Element

Let’s look at _ModalScope. _ModalScope is a StatefulWidget, and we can understand it as a StatefulWidget.

Look at the initState method first

@override
void initState() {
  super.initState();
  final List<Listenable> animations = <Listenable>[
    if(widget.route.animation ! =null) widget.route.animation! .if(widget.route.secondaryAnimation ! =null) widget.route.secondaryAnimation!,
  ];
  _listenable = Listenable.merge(animations); / / first place
  if(widget.route.isCurrent && _shouldRequestFocus) { widget.route.navigator! .focusScopeNode.setFirstFocus(focusScopeNode);/ / in the second place}}Copy the code

Merge the Listenable function. Merge the Listenable function. Merge the Listenable function. This allows _listEnable to respond to both animation and secondaryAnimation changes.

Second: the focus scope is moved to the current scope

For the focusScopeNode, see this article: Talk about Focus, the unsung hero of Flutter

Let’s look at the build method, which is the highlight.

@override
Widget build(BuildContext context) {
  return AnimatedBuilder( / / first place
    animation: widget.route.restorationScopeId,
    builder: (BuildContext context, Widget? child) {
      return RestorationScope(
        restorationId: widget.route.restorationScopeId.value,
        child: child!,
      );
    },
    child: _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: Builder(
            builder: (BuildContext context) {
              return Actions(
                actions: <Type, Action<Intent>>{
                  DismissIntent: _DismissModalAction(context),
                },
                child: PrimaryScrollController(
                  controller: primaryScrollController,
                  child: FocusScope(
                    node: focusScopeNode, // immutable
                    child: FocusTrap(
                      focusScopeNode: focusScopeNode,
                      child: RepaintBoundary(
                        child: AnimatedBuilder(
                          animation: _listenable, // immutable
                          builder: (BuildContext context, Widget? child) {
                            returnwidget.route.buildTransitions( context, widget.route.animation! , widget.route.secondaryAnimation! , AnimatedBuilder( animation: widget.route.navigator? .userGestureInProgressNotifier ?? ValueNotifier<bool> (false),
                                builder: (BuildContext context, Widget? child) {
                                  final boolignoreEvents = _shouldIgnoreFocusRequest; focusScopeNode.canRequestFocus = ! ignoreEvents;returnIgnorePointer( ignoring: ignoreEvents, child: child, ); }, 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

The component hierarchy is very deep, but the components are relatively simple, so let’s look at them layer by layer.

Layer 1: AnimatedBuilder at site 1. AnimatedBuilder is an animation component. The component that you want to animate is the child property, \_ModalScopeStatus. The animation that you add is the animation property, restorationScopeId of route. We introduced it in the member variables section. This layer adds animation to restorationScopeId without affecting the actual display.

Layer 2: _ModalScopeStatus, which is a normal InheritedWidget that passes down the state of ModalScope (who Route is, whether it’s a top-level Route, whether it can Pop).

The third layer: Offstage. Whether the current is not on “stage”, not on stage layout is not drawn. That is, Route is not drawn when the offstage property of Route is true. The default value is false. However, if the page Hero animation, the HeroController controller will use this value to control the effect of the later page. We just need to know that this value doesn’t matter to us.

Level 4: PageStorage, which provides a bucket for storing data for elements in a page. The child node can obtain the bucket through pagestorage.of. For example, ScrollPosition uses this feature to store the offset of a scroll.

@protected 
voidsaveScrollOffset() { PageStorage.of(context.storageContext)? .writeState(context.storageContext, pixels); }Copy the code

The fifth layer: Actions, Actions is the new action β€”β€”β€”β€” intent component, Route DismissIntent corresponds to _DismissModalAction, The navigator.of (context).maybepop () behavior is performed. But this addition doesn’t have much impact on our mobile devices.

Level 6: PrimaryScrollController is the default ScrollController, which is why we still have a ScrollController available even if we don’t manually add a ScrollController to the ListView.

Level 7: FocusScope, which adds a focus domain to the page and provides a focus.

Layer 8: FocusTrap, which is a component for the Web and has little to do with our mobile terminal.

Level 9: RepaintBoundary: add a drawn boundary to the page, that is, nodes above this node do not need to be drawn.

When we talked about layout, we talked about the need for boundaries in layout. If the parent node depends on the size information of the child node, then when the child node needs to be laid out, the parent node will also be laid out. πŸ‘‰Flutter must know must Know series – Render tree layout

Similarly, drawing requires boundaries. If a node needs to be drawn, it looks at the isRepaintBoundary value, and if it’s true, it just draws itself and doesn’t look up at the parent node. RepaintBoundary the isRepaintBoundary value of the component is true.

The first few layers are all preparation for a page, and the tenth layer below is where the real complexity comes in.

The tenth layer is still an AnimatedBuilder animation component that animates Route. AnimatedBuilder is the Builder effect that adds one parameter to the constructor’s child argument. The driver for animation is the parameter _listenable. Remember who _listenable is? It is the merger of animation and secondaryAnimation.

The basic model is as follows:

AnimatedBuilder(
  animation: _listenable, 
  builder: (BuildContext context, Widget child) {
    return B;
  },
  child: A,
)
Copy the code

We can assume that when the value of _listenable (that is, the value of the animation) changes, the page displays B. And the child argument to builder is A.

Now let’s see who A is.

RepaintBoundary(
  key: widget.route._subtreeKey, // immutable
  child: Builder(
    builder: (BuildContext context) {
      returnwidget.route.buildPage( context, widget.route.animation! , widget.route.secondaryAnimation! ,); },),)Copy the code

A is the Builder component, so A is the Widget built by Route’s buildPage method.

Similarly, the child of the parameter in B is the result of the buildPage method. Let’s see who B is.

widget.route.buildTransitions( context, widget.route.animation! , widget.route.secondaryAnimation! , AnimatedBuilder( animation: widget.route.navigator? .userGestureInProgressNotifier ?? ValueNotifier<bool> (false),
    builder: (BuildContext context, Widget? child) {
      final boolignoreEvents = _shouldIgnoreFocusRequest; focusScopeNode.canRequestFocus = ! ignoreEvents;return IgnorePointer(
        ignoring: ignoreEvents,
        child: child,
      );
    },
    child: child,
  ),
)
Copy the code

B is the Widget for Route’s buildTransitions build.

AnimatedBuilder appears again in B. This animation does not affect the rendering of the page, but depends on the animation state of the route to determine whether the screen does not block gestures and whether to display builds.

So now we know that layer 10 uses the animation mechanism, and the component for the animation is the Route buildPage method, and the specific animation is the Route buildTransitions method.

For example, when the animation is 0, we let buildTransitions return child, and the page displays the Route’s buildPage. When animation is 0.5, we let buildTransitions return the Text Text component, which is what the page displays. When the animation is 1, we let buildTransitions return child, and the page will still display the Route’s buildPage. So with this feature, we can show panning, zooming and so on.

At this point, modalroute. install is initialized and two overlayentries are constructed. One is a mask, which handles colors, clicking back, and so on. One is _ModalScope, which has more than ten nodes: it realizes basic Modal Route information transmission, focus, drawing not drawing, data bucket, drawing boundary, animation and so on.

ModalRoute initialization, basically is the initialization of Route, function basic realization. Developers simply implement buildPage and buildTransitions without technical details, which could be a template pattern.

BuildPage is the content for the page display, and buildTransitions is the animated display of the content.

RawDialogRoute.install

ModalRoute above has done most of its work and defines the inheritance tasks of subclasses, overriding buildPage to determine what to display and overriding buildTransitions to determine transitions.

As the Route of the concrete dialog box, it only needs to implement these two methods. Let’s use animation as an example:

@override
Widget buildTransitions(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
  if (_transitionBuilder == null) {
    return FadeTransition(
      opacity: CurvedAnimation(
        parent: animation,
        curve: Curves.linear,
      ),
      child: child,
    );
  } // Some default transition
  return_transitionBuilder! (context, animation, secondaryAnimation, child); }Copy the code

We see that the default animation effect for the dialog box is defined in buildTransitions — linear fade animation.

summary

We looked at the Route initialization process for each layer, from the top to the concrete reading. We know:

  1. The Route of theOverlayEntryArray, you’re going to add Overlay to the Navigator, so the page is superimposed.
  2. Each layer has its own embodiment.OverlayRouteTo complete theOverlayEntryTo add,TransitionRoute.installAnimation and animation controller are generated.
  3. ModalRoute.installIt is a combination of mask and animation effects.buildPageIs what’s displayed,buildTransitionsAnimation is the content displayed at any time, through the developer is the animation progress, we can achieve their own effects according to the animation progress.
  4. PageRouteIs the parent of the page Route,RawDialogRouteIs the Route of the dialog box that provides the defaultFadeTransitionThe animation.
  5. The page’s fence istrue, so it does not layout and draw blocked pages.

Route linear release

Having looked at linear initialization, let’s look at linear freeing resources. Releasing resources is the Dispose method. Compared to the initialization, the release is much simpler, basically the request is released.

Route.dispose

void dispose() {
  _navigator = null;
}
Copy the code

No more references to navigator to avoid memory leaks.

OverlayRoute.dispose

@override
void dispose() {
  _overlayEntries.clear();
  super.dispose();
}
Copy the code

Corresponding to the initialization of OverlayRoute, OverlayEntry is constructed with createOverlayEntries.

Therefore, OverlayEntry is removed from dispose.

TransitionRoute.dispose

@override
voiddispose() { _animation? .removeStatusListener(_handleStatusChanged);if(willDisposeAnimationController) { _controller? .dispose(); } _transitionCompleter.complete(_result);super.dispose();
}
Copy the code

Dispose dispose of the Controller corresponding to the initialization of the TransitionRoute, which constructs the animation Controller.

At this point, the resources held by the initialization have been released, and subsequent calls do not need to be made.

conclusion

So far, we have basically understood what Route is, and have a certain understanding of the hierarchical system and structure of Route. For us developers, we just need to know:

  • Pages are fenced, so you don’t have to worry about drawing non-top-level pages
  • inbuildPageDisplay the content we want inbuildTransitionsAccording to the animation progress, to achieve their own effects.

Based on this section, we can take a closer look at Route push and POP flows later.