As a new generation of routing, navigator 2.0 provides a declarative API, which is more in line with the style of fluent. Navigator 2.0 is forward compatible, and some new APIs have been added, which is quite different from Navigator 1.0.
This article will analyze the underlying logic of Navigator 2.0 in detail, so that we can have an in-depth understanding of it, which will be more handy in use.
Background of Navigator 2.0
There are several main reasons why the official team of Flutter modified the route:
- Navigator 1.0 only provides some simple API s such as push(), pushNamed(), and pop(). It is difficult to push in or pop up multiple pages, and it is more difficult to remove and exchange the middle pages in the stack;
- With the arrival of 2.0, Flutter has realized full platform support, which leads to some new usage scenarios, such as web page modifying URL address, which need new API support;
- Navigator 2.0 meets the demand scenario of nested routing, which makes it more flexible and convenient for developers to use;
- Navigator 2.0 provides a declarative API, which solves the previous way of routing imperative programming and makes the programming style unified.
Although there are many API s for Navigator 2.0, the logic is still relatively clear. Let's introduce them one by one.
Page
Page represents the immutable configuration information of a page and represents a page. Similar to the conversion of Widget configuration information into element, the information configured by page will be converted into Route.
abstract class Page<T> extends RouteSettings { const Page({ this.key, String? name, Object? arguments, this.restorationId, }) : super(name: name, arguments: arguments); bool canUpdate(Page<dynamic> other) { return other.runtimeType == runtimeType && other.key == key; } @factory Route<T> createRoute(BuildContext context); } Copy code
- createRoute is the method of converting to Route;
- canUpdate is implemented in the same way as Widget and is also used for diff algorithm.
RouteSettings
The parent class RouteSettings of Page is only used to save the values of name and arguments.
const RouteSettings({ this.name, this.arguments, }); Copy code
Route
Route represents a page, which is really managed in the Navigator stack.
abstract class Route<T> { // 1 RouteSettings get settings => _settings; NavigatorState? get navigator => _navigator; // 2 List<OverlayEntry> get overlayEntries => const <OverlayEntry>[]; // 3 void install() {} TickerFuture didPush() {} ... } Copy code
- Route holds the configuration object page and the navigator object that manages it;
- Route also holds an Overlay entry array, which is placed on an Overlay similar to Stack. The page we write is placed on an Overlay entry;
- Route also defines that some protocol methods need to be overridden by subclasses. These methods are mainly callback functions received after the state of route changes. These function calls mainly come from_ RouteEntry.
method | Call timing |
---|---|
install | Inserted into navigator |
didPush | Animation entry display |
didAdd | Direct display |
didReplace | Replace old route |
didPop | Request pop page |
didComplete | After pop |
didPopNext | The route behind the current route is pop |
didChangeNext | The route following the current route is replaced |
didChangePrevious | The route in front of the current route is replaced |
changedInternalState | After the state of the current route changes |
changedExternalState | After the navigator of the current route changes |
MaterialPage and_ PageBasedMaterialPageRoute
We can directly use the Page class provided by the system or customize the class inherited from Page. Let's take a look at the logic of the MaterialPage provided by the official.
The route of MaterialPage is_ Pagebasedmaterialpageroute class. Its inheritance logic is:_ PageBasedMaterialPageRoute -> PageRoute -> ModalRoute -> TransitionRoute -> OverlayRoute + LocalHistoryRoute -> Route.
LocalHistoryRoute
LocalHistoryRoute can add some localhistoryentries to the Route. When the LocalHistoryEntry is not empty, the last LocalHistoryEntry will be removed when the didPop method is called, otherwise the Route will be pop.
OverlayRoute
OverlayRoute mainly holds the OverlayEntry array corresponding to the Route, which is assigned by the subclass when it is inserted into the navigator.
abstract class OverlayRoute<T> extends Route<T> { @factory Iterable<OverlayEntry> createOverlayEntries(); List<OverlayEntry> get overlayEntries => _overlayEntries; void install() { _overlayEntries.addAll(createOverlayEntries()); super.install(); } } Copy code
TransitionRoute
TransitionRoute is mainly responsible for the animation part.
abstract class TransitionRoute<T> extends OverlayRoute<T> { Animation<double>? get animation => _animation; Animation<double>? get secondaryAnimation => _secondaryAnimation; void install() { _animation = createAnimation() ..addStatusListener(_handleStatusChanged); super.install(); } TickerFuture didPush() { super.didPush(); return _controller!.forward(); } void didAdd() { super.didAdd(); _controller!.value = _controller!.upperBound; } bool didPop(T? result) { _controller!.reverse(); return super.didPop(result); } void didPopNext(Route<dynamic> nextRoute) { _updateSecondaryAnimation(nextRoute); super.didPopNext(nextRoute); } void didChangeNext(Route<dynamic>? nextRoute) { _updateSecondaryAnimation(nextRoute); super.didChangeNext(nextRoute); } } Copy code
- TransitionRoute has_ Animation and secondaryAnimation. The former is responsible for the push and pop animation of the current Route, and the latter is responsible for the animation of the next Route when it is pushed and pop.
- _ animation is generated by install ing. secondaryAnimation can be the next Route in most cases_ animation, so the secondaryAnimation needs to be updated when didPopNext and didChangeNext.
- If the didAdd method is called when animation is not needed, Route is a passive method, which is actually_ RouteEntry judges the method to be called according to the state (determined by the Navigator).
ModalRoute
The main function of ModalRoute is to prevent user interaction between routes other than the top-level Route, and the knowledge points are also very rich.
abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T> { Iterable<OverlayEntry> createOverlayEntries() sync* { yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier); yield _modalScope = OverlayEntry(builder: _buildModalScope, maintainState: maintainState); } } Copy code
- ModalRoute generates two very important overlayentries---_ Modal barrier and_ modalScope.
- _ Modal barrier realizes the function of preventing users from interacting with routes other than the top Route;
- _ modalScope will hold the router itself_ When modalScope is built, it will call the buildTransitions and buildChild methods of router. The parameters include the animation and secondaryAnimation of router, that is, the two animation attributes in TransitionRoute;
Widget _buildModalScope(BuildContext context) { return _modalScopeCache ??= Semantics( sortKey: const OrdinalSortKey(0.0), child: _ModalScope<T>( key: _scopeKey, route: this, // _ModalScope calls buildTransitions() and buildChild(), defined above ) ); } Widget buildPage(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation); Widget buildTransitions( BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child, ) { return child; } Copy code
Let's see next_ ModalScope_ Contents of ModalScopeState:
class _ModalScopeState<T> extends State<_ModalScope<T>> { late Listenable _listenable; final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: '$_ModalScopeState Focus Scope'); 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); if (widget.route.isCurrent) { widget.route.navigator!.focusScopeNode.setFirstFocus(focusScopeNode); } } } Copy code
- _ listenable is a combination of route's animation and secondaryAnimation;
- focusScopeNode is the focus. During initialization, set the focus of the navigator to this focus, so that the Route at the top layer can obtain the focus and shield the focus acquisition of other routes;
Widget build(BuildContext context) { // 1 RestorationScope return AnimatedBuilder( animation: widget.route.restorationScopeId, builder: (BuildContext context, Widget? child) { return RestorationScope( restorationId: widget.route.restorationScopeId.value, child: child!, ); }, // 2 _ModalScopeStatus 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 // 3 RepaintBoundary child: RepaintBoundary( // 4. AnimatedBuilder child: AnimatedBuilder( animation: _listenable, // immutable builder: (BuildContext context, Widget? child) { // 5. buildTransitions return 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 bool ignoreEvents = _shouldIgnoreFocusRequest; focusScopeNode.canRequestFocus = !ignoreEvents; return IgnorePointer( ignoring: ignoreEvents, child: child, ); }, 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!, ); }, ), ), ), ), ), ), ); }, ), ), ), ), ); } Copy code
_ The build method of modalscope state is a very sophisticated method:
- RestorationScope is responsible for the role of Route in recovering data;
- _ ModalScopeStatus is InheritedWidget, which keeps the reference to Route, so we are calling modalroute Of (content) gets the page parameters through this_ ModalScopeStatus, and then find the corresponding parameter.
- A RepaintBoundary is placed in the middle to limit the redrawn area, which can improve the efficiency of animation;
- The bottom animatedbuilder, the Widget is the core, and the child of the animatedbuilder is composed of Route Buildpage () is actually the child of our Page, that is, the Page content written by the developer; This AnimatedBuilder's builder method calls Route.. Buildtransitions(), which drives animation_ Listening, that is to say, both animation and secondaryAnimation can drive its animation process. This is actually easy to understand: the pop and push of the current Route and the pop and push of the next Route will trigger the generation of animation.
PageRoute
PageRoute is mainly to make the Route below the top layer invisible. Click_ Modal barrier does not allow the current Route to pop up from the Navigator stack.
abstract class PageRoute<T> extends ModalRoute<T> { @override bool get opaque => true; @override bool get barrierDismissible => false; } Copy code
_PageBasedMaterialPageRoute
_ The function of PageBasedMaterialPageRoute is to override the buildPage method and return the interface written by the developer;
class _PageBasedMaterialPageRoute<T> extends PageRoute<T> with MaterialRouteTransitionMixin<T> { Widget buildContent(BuildContext context) { return _page.child; } } Copy code
The official provides us with default pop and push animations, which are implemented in the mixed MaterialRouteTransitionMixin. MaterialRouteTransitionMixin will be implemented differently according to different platforms. iOS is left-right animation, Android is up-down animation, and the web is also left-right animation.
We take iOS as an example, and finally use the method of CupertinoPageTransition:
SlideTransition( position: _secondaryPositionAnimation, textDirection: textDirection, transformHitTests: false, child: SlideTransition( position: _primaryPositionAnimation, textDirection: textDirection, child: DecoratedBoxTransition( decoration: _primaryShadowAnimation, child: child, ), ) Copy code
Are you confused to see that SlideTransition is nested on a child? Two animations on a Widget?
Explain the other parameters first:
- textDirection determines the sliding method, because some languages are sorted from right to left;
- transformHitTests is set to false, and the response position of click events is not affected by the animation;
- _ primaryShadowAnimation sets the shadow in an animation.
_ secondaryPositionAnimation is from offset Zero to Offset(-1.0/3.0, 0.0). Normally, it is to move 1 / 3 of the screen width from right to left.
final Animatable<Offset> _kMiddleLeftTween = Tween<Offset>( begin: Offset.zero, end: const Offset(-1.0/3.0, 0.0), ); Copy code
_ primaryPositionAnimation is from Offset(1.0, 0.0) to offset Zero, under normal circumstances, is to move from the right side of the invisible screen to the leftmost side of the screen, and then occupy the whole screen width.
final Animatable<Offset> _kRightMiddleTween = Tween<Offset>( begin: const Offset(1.0, 0.0), end: Offset.zero, ); Copy code
Next, let's explain the animation logic when pop a Route, animation: 0 - > 1
- The newly added Route is_ The primaryPositionAnimation is directly driven, that is, it performs the right to left mapping_ kRightMiddleTween animation;
- _ The value of secondaryPositionAnimation has only been modified. As mentioned in the introduction of TransitionRoute earlier, the animation of the newly added Route is assigned to the secondaryAnimation attribute of the previous Route_ Modalscope state has introduced that secondaryAnimation can also drive Route animation, that is, the previous Route can also generate one_ kMiddleLeftTween animation;
generalization:
The newly added Route drives the animation from the right to the left of the screen through the animation. The animation is assigned to the secondaryAnimation of the previous Route to drive the previous Route to move 1 / 3 of the screen position to the left.
The logic of push is similar, just a reverse animation reverse. The previous Route is driven by secondaryAnimation to move 1 / 3 of the screen width to the right, and the current Route is driven by animation to move out of the screen.
We can click on the Slow Animations of Flutter DevTools to see the slow animation process:
Stage summary
_RouteEntry
Navigator is not a Route operated directly, but an encapsulated class of Route_ RouteEntry.
_RouteEntry( this.route, { required _RouteLifecycle initialState, this.restorationInformation, }) Copy code
_ RouteEntry holds a route in addition to a route_ Routelife, i.e. route status.
Functions are mainly modified_ Routelife status functions, such as markForPush,markForAdd,markForPop,markForRemove,markForComplete, etc. Besides_ After routelife is marked, it performs operation functions on Route, such as handlePush, handleAdd,handlePop,remove, etc.
Navigator
Navigator({ Key? key, this.pages = const <Page<dynamic>>[], // ... }) Copy code
There is a key attribute pages in the construction method of Navigator. The Navigator will convert the incoming pages into the corresponding pages of Routes_ RouteEntry array.
The logic to realize declarative programming is to modify the contents in the pages, and the navigator will automatically realize the corresponding jump, return, replacement and other operations. Navigator.push,Navigator.pop and other previously used methods will not be considered by developers.
Let's next analyze the important code of navigator state.
class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin { List<_RouteEntry> _history = <_RouteEntry>[]; late GlobalKey<OverlayState> _overlayKey; OverlayState? get overlay => _overlayKey.currentState; final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope'); } Copy code
- _ history is generated by each Page in the pages through createRoute_ RouteEntry array;
- OverlayStateoverlay represents OverLay, which is responsible for placing the OverLay entries array of each Route; OverLay is equivalent to a Stack, which is specially used to place OverLay entries.
The core method of the navigator state is the didUpdateWidget method, which calls a_ updatePages() method:
void didUpdateWidget(Navigator oldWidget) { _updatePages(); } Copy code
_ The main function of the updatePages method is to perform diff comparison and update the pages_ Each in the history array_ routeEntry_ RouteLifecycle, finally call flushHistoryUpdates() method.
_ The routeEntry comparison method is the same as that of MultiChildRenderObjectElement. Previously, the reusable elements are compared from back to front, and then the reusable elements are compared from back to front. Then the remaining elements are reused or newly created, and the non reusable elements are destroyed.
void _flushHistoryUpdates({bool rearrangeOverlay = true}) { final List<_RouteEntry> toBeDisposed = <_RouteEntry>[]; while (index >= 0) { switch (entry!.currentState) { case _RouteLifecycle.push: case _RouteLifecycle.pushReplace: case _RouteLifecycle.replace: entry.handlePush( navigator: this, previous: previous?.route, previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route, isNewFirst: next == null, ); if (entry.currentState == _RouteLifecycle.idle) { continue; } break; // ... } index -= 1; next = entry; entry = previous; previous = index > 0 ? _history[index - 1] : null; } _flushObserverNotifications(); _flushRouteAnnouncement(); for (final _RouteEntry entry in toBeDisposed) { for (final OverlayEntry overlayEntry in entry.route.overlayEntries) overlayEntry.remove(); entry.dispose(); } if (rearrangeOverlay) { overlay?.rearrange(_allRouteOverlayEntries); } } Copy code
- According to each_ RouteEntry_ Routelife calls the corresponding method, for example, if Route is marked as_ RouteLifecycle.push, then call the handlePush method, so that the Route will call the install method to insert it into the tree of the Navigator, and then conduct animation;
- _ Flushobserver notifications are for each_ The navigator observation listener notifies;
- _ Flushroute announcement is mainly used to sort out and update the relationship between each Route. The update of secondaryAnimation is carried out at this time;
- Will not be needed_ The overlayEntries of RouteEntry are removed from the Overlay because they no longer need to be displayed;
- Then put all the_ The overlayEntries of RouteEntry are updated to Overlay. The code can be seen in the build method. The added logic is as follows.
Widget build(BuildContext context) { return HeroControllerScope.none( child: Listener( onPointerDown: _handlePointerDown, onPointerUp: _handlePointerUpOrCancel, onPointerCancel: _handlePointerUpOrCancel, child: AbsorbPointer( absorbing: false, // it's mutated directly by _cancelActivePointers above child: FocusScope( node: focusScopeNode, autofocus: true, child: UnmanagedRestorationScope( bucket: bucket, child: Overlay( key: _overlayKey, initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[], ), ), ), ), ), ); } Copy code
By the way, HeroControllerScope is a Widget responsible for Hero animation, which is similar to shared element animation in Android.
Stage summary
So far, we can realize route switching by switching the page of Navigator. Is this the end of the article? No, because Navigator 2.0 is built for the full platform of fluent 2.0, some problems have not been solved, such as editing browser URL, web page return, Android physical key return and other functions.
Router
Router({ Key? key, this.routeInformationProvider, this.routeInformationParser, required this.routerDelegate, this.backButtonDispatcher, }) final RouteInformationProvider? routeInformationProvider; final RouteInformationParser<T>? routeInformationParser; final RouterDelegate<T> routerDelegate; final BackButtonDispatcher? backButtonDispatcher; Copy code
We can see that the Router has four attributes: RouteInformationProvider, routing information provider, RouteInformationParser, routing information resolver, RouterDelegate processing agent of routing information, and backbuttonddispatcher returns the distributor of processing. They work together to realize the function of routing.
RouteInformation
The routing information mentioned above refers to RouteInformation, including the route location of the route and the state corresponding to the route. The State referred to here is data.
class RouteInformation { final String? location; final Object? state; } Copy code
RouteInformationProvider
RouteInformationProvider has only one abstract method routerReportsNewRouteInformation, which is used to perform some additional operations based on RouteInformation.
abstract class RouteInformationProvider extends ValueListenable<RouteInformation?> { void routerReportsNewRouteInformation(RouteInformation routeInformation) {} } Copy code
By default, the system uses the PlatformRouteInformationProvider. Its routerReportsNewRouteInformation method calls back the update of the system route. For example, the browser will add a History access record in the History stack:
class PlatformRouteInformationProvider extends RouteInformationProvider with WidgetsBindingObserver, ChangeNotifier { void routerReportsNewRouteInformation(RouteInformation routeInformation) { SystemNavigator.routeInformationUpdated( location: routeInformation.location!, state: routeInformation.state, ); _value = routeInformation; } } Copy code
RouteInformationParser
This class is used to convert T-page model and RouteInformation routing information to each other:
abstract class RouteInformationParser<T> { Future<T> parseRouteInformation(RouteInformation routeInformation); RouteInformation? restoreRouteInformation(T configuration) => null; } Copy code
parseRouteInformation is mainly used when parsing the initial route. For example, the startup page is displayed according to RouteInformation(location: "/");
restoreRouteInformation is to generate the corresponding RouteInformation according to the T page model.
RouterDelegate
RouterDelegate, as its name implies, is a class that replaces the Router. It includes adding a page according to the T page model, pop a page, providing the content of the build, etc.
abstract class RouterDelegate<T> extends Listenable { Future<void> setInitialRoutePath(T configuration) { return setNewRoutePath(configuration); } Future<void> setNewRoutePath(T configuration); Future<bool> popRoute(); T? get currentConfiguration => null; Widget build(BuildContext context); } Copy code
The popRoute method of PopNavigatorRouterDelegateMixin can be mixed in, so you don't have to implement it yourself.
From the perspective of source code, let's see how RouteInformationProvider, RouteInformationParser and RouterDelegate are implemented in initializing Routing:
class _RouterState<T> extends State<Router<T>> { void initState() { super.initState(); if (widget.routeInformationProvider != null) { _processInitialRoute(); } } void _processInitialRoute() { _currentRouteInformationParserTransaction = Object(); _currentRouterDelegateTransaction = Object(); _lastSeenLocation = widget.routeInformationProvider!.value!.location; widget.routeInformationParser! .parseRouteInformation(widget.routeInformationProvider!.value!) .then<T>(_verifyRouteInformationParserStillCurrent(_currentRouteInformationParserTransaction, widget)) .then<void>(widget.routerDelegate.setInitialRoutePath) .then<void>(_verifyRouterDelegatePushStillCurrent(_currentRouterDelegateTransaction, widget)) .then<void>(_rebuild); } } Copy code
In_ In the processInitialRoute method, we can see that routeInformationParser parses the value of routeInformationProvider, and then routerDelegate calls setNewRoutePath to set the route according to the parsing result.
routeInformationProvider -> routeInformationParser -> routerDelegate -> (setNewRoutePath)
Override case of RouterDelegate:
class MyRouterDelegate extends RouterDelegate<PageConfiguration> with ChangeNotifier, PopNavigatorRouterDelegateMixin<PageConfiguration> { final List<Page> _pages = []; final AppState appState; final GlobalKey<NavigatorState> navigatorKey; MyRouterDelegate(this.appState) : navigatorKey = GlobalKey() { appState.addListener(() { notifyListeners(); }); } List<MaterialPage> get pages => List.unmodifiable(_pages); Future<bool> popRoute() { _removePage(_pages.last); return Future.value(false); } Future<void> setNewRoutePath(PageConfiguration configuration) { if (shouldAddPage) { _pages.clear(); addPage(configuration); } return SynchronousFuture(null); } Widget build(BuildContext context) { return Navigator( key: navigatorKey, onPopPage: _onPopPage, pages: buildPages(), ); } } Copy code
- MyRouterDelegate has_ Pages attribute, which is used as the pages of the Navigator; appState is the data of state management. Use this data to drive the observer of MyRouterDelegate, that is, the Router to reconstruct, so that the Navigator will be reconstructed.
- popRoute will_ Delete the last page of the pages and notify the Router to reconstruct and update the Navigator;
- setNewRoutePath to_ pages adds the corresponding Page and informs the Router to reconstruct the Navigator.
BackButtonDispatcher
Backbuttonddispatcher is mainly used to solve the physical return events of Android and web pages. It has two subclasses RootBackButtonDispatcher and ChildBackButtonDispatcher, which can solve the nesting problem of Router.
The return processing of backbuttonddispatcher can be directly handed over to RouterDelegate for processing, such as the following logic:
class MyBackButtonDispatcher extends RootBackButtonDispatcher { final MyRouterDelegate _routerDelegate; MyBackButtonDispatcher(this._routerDelegate) : super(); // 3 @override Future<bool> didPopRoute() { return _routerDelegate.popRoute(); } } Copy code
Final summary
summary
Navigator 2.0 is more powerful, and its usage has become more fluent. But it has become more complex, which has caused great trouble to the learning and use cost. This is also the reason why many people think navigator 2.0 is a failed transformation.
This paper mainly analyzes the implementation logic of Navigator 2.0 from the perspective of source code. It should be very simple to write code after the principle is clear.
If you need a Demo, you can refer to the code of the following two articles, especially the code of the first article, which is of great reference value: