The entire flutter application runs only based on a view in the native application, such as FlutterView in android. Page switching in flutter depends on its routing mechanism, that is, a set of routing functions centered on Navigator, which enables it to complete a page switching effect similar to native and customizable.
The following will introduce the routing implementation principle in flutter, including page loading during initialization, the underlying mechanism for switching pages, etc.
Realize the basics
The operation of flutter applications requires the operation of the two widgets, MaterialApp/CupertinoApp. They correspond to the design style of android/ios respectively. They also provide some basic facilities for the operation of the application, such as the main page and routing table related to routing, and the theme and locale related to the overall page display.
Several configurations related to routing include home, routes, initialRoute, onGenerateRoute, and onUnknownRoute, which correspond to the main page widget, routing table (the corresponding widget is found based on the route), the route at first load, the route generator, and unknown routing agent (such as the common 404 page).
The subnodes of MaterialApp/CupertinoApp are all WidgetsApp, but they pass different parameters to WidgetsApp, which makes the interface styles of the two Widgets inconsistent. Navigator is created in WidgetsApp.
Widget build(BuildContext context) { Widget navigator; if (_navigator != null) { navigator = Navigator( key: _navigator, // If isn't '/', we should assume it was set // intentionally via `setInitialRoute`, and should override whatever // is in []. initialRoute: != ? : ?? , onGenerateRoute: _onGenerateRoute, onGenerateInitialRoutes: == null ? : (NavigatorState navigator, String initialRouteName) { return (initialRouteName); }, onUnknownRoute: _onUnknownRoute, observers: , ); } ... }
The first one created in the WidgetsApp build is Navigator. Let’s take a look at its parameters. First of all, _navigator is a GlobalKey, which allows WidgetsApp to call Navigator’s function through key to perform route switching. That is, when processing native routing switching information in WidgetsBinding, it is finally done by WidgetsApp. In addition, the _navigator here should only be used in WidgetsApp. If you need to use it, you are generally called directly to obtain it. This function will find NavigatorState upward along the element tree, so switching routes in the application requires Navigator to be wrapped. However, since Navigator is generated in WidgetsApp, you don’t have to consider these in development.
In addition, it is about the way to obtain the upper NavigatorElement instances at the bottom. There are two ways to obtain the upper instances from the bottom in the Element tree. One is to use the InheritedWidget, and the other is to directly search upward along the tree (ancestorXXXOfExactType series). The principles of the two methods are basically the same, but the InheritedWidget will be passed down layer by layer during the establishment of the tree, and the latter will only look upward when used. Therefore, from this perspective, using the InheritedWidget will be more efficient, but the advantages of InheritedWidget are not only that. It can notify all nodes that depend on it for updates when the data changes, which is also not available in the AncestorXXXOfExactType series.
Then initialRoute specifies the page at initialization, which is determined by and , but the former has a higher priority because this is specified in native. Taking android as an example, when starting FlutterActivity, you can pass in the route field to specify the initialization page.
onGenerateRoute and onUnknownRoute are strategies to obtain route. When onGenerateRoute does not hit, onUnknownRoute will be called. Given a default page, onGenerateInitialRoutes is used to produce the route list when starting the application. It has a default implementation of defaultGenerateInitialRoutes, which will select different Routes based on the passed initialRouteName. If the passed initialRouteName is not the default main page route, flutter will not use initRoute as the main page, but will put the default route on the stack and then enter the corresponding page of initRoute. Therefore, if popRoute is called after this, it will return to the main page.
observers is a listener list for route switching, which can be passed in from outside and do some operations during route switching. For example, HeroController is a listener.
Navigator is a StatefulWidget. The process of converting initRoute to Route is completed in the initState of NavigatorState, and calls push to put it on the stack to generate an OverlayEntry. This will continue to be passed to the lower layer responsible for displaying the page. The Overlay responsible for displaying it.
During pushing, route will be converted into an OverlayEntry list stored, and each OverlayEntry stores a WidgetBuilder. From a certain perspective, OverlayEntry can be considered a page. The coordination and display of all pages is done through Overlay, which is a Stack-like structure that can display multiple sub-nodes. In its initState,
void initState() { (); insertAll(); }
The initialEntries will be saved in _entries.
As a control that can determine the display page based on the route, Overlay is actually relatively 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) { (_OverlayEntry(entry)); if () onstage = false; } else if () { (TickerMode(enabled: false, child: _OverlayEntry(entry))); } } return _Theatre( onstage: Stack( fit: , children: (growable: false), ), offstage: offstageChildren, ); }
In the build function, all OverlayEntry is divided into two parts: visible and invisible. Each OverlayEntry generates a _OverlayEntry, which is a StatefulWidget. Its function is mainly to control the current page repaint, which is encapsulated and then displayed with _Theatre. In _Theatre, the visible/invisible child nodes will be converted into Element, but when drawing, the _RenderTheatre corresponding to _Theatre will only draw the visible child nodes.
Determine whether an OverlayEntry can completely obscure the previous OverlayEntry. It is judged by its opaque variable, and opaque is given by Route. When the page animation is executed, this value will be set to false. After the page switching animation is executed, the opaque parameter of the Route will be assigned to its OverlayEntry. Generally speaking, the Route corresponding to the window is false, and the Route corresponding to the page is true.
So after the page switches, the previous page always exists in the element tree, but it is not drawn in the RenderObject, which can also be reflected in the Flutter Outline tool. From this perspective, it can also be understood that the more pages there are in flutter, the more steps you need to process. Although there is no need to draw the bottom page, there will still be basic traversal of the entire tree, which is also considered overhead.
_routeNamed
The main dependency is the routing management system for page management in flutter. Its entrance is Navigator. What it manages is essentially the Route that carries the user page. However, in Navigator, there are many functions in the XXXName series. They are not Route, but RouteName. According to personal understanding, this is mainly for easy development introduction. We can directly pass the routing table in MaterialApp/CupertinoApp. Each name corresponds to a WidgetBuilder, and then combine it with pageRouteBuilder (this can be customized, but MaterialApp/CupertinoApp has a default implementation, which can convert WidgetBuilder into Route), and then convert it from RouteName to Route.
Route<T> _routeNamed<T>(String name, { @required Object arguments, bool allowNull = false }) { if (allowNull && == null) return null; final RouteSettings settings = RouteSettings( name: name, arguments: arguments, ); Route<T> route = (settings) as Route<T>; if (route == null && !allowNull) { route = (settings) as Route<T>; } return route; }
This process is divided into three steps, generating RouteSettings, calling onGenerateRoute to get the corresponding route from the routing table, and if there is no hit, calling onUnknownRoute to give something similar to the 404 page.
onGenerateRoute and onUnknownRoute are passed in when building Navigator and implemented in WidgetsApp.
Route<dynamic> _onGenerateRoute(RouteSettings settings) { final String name = ; final WidgetBuilder pageContentBuilder = name == && != null ? (BuildContext context) => : [name]; if (pageContentBuilder != null) { final Route<dynamic> route = <dynamic>( settings, pageContentBuilder, ); return route; } if ( != null) return (settings); return null; }
If it is the default route, the given home page will be used directly (if there is one), otherwise, you will go directly to the routing table to check, so in essence, the home page here is more of a symbol, a symbol of identity, and it doesn't matter if it doesn't. In addition, the main output of the routing table is WidgetBuilder. It needs to be packaged once and becomes a Route to be the finished product. If you don’t want to use the routing table, you can also directly implement the onGenerateRoute function and generate the Route directly according to RouteSetting. This is not just as simple as returning a WidgetBuilder, but you need to wrap it yourself.
onUnknownRoute is mainly used to provide a page similar to 404, which also requires a direct return to Route.
_flushHistoryUpdates
I don’t know which version to start, flutter’s routing management introduced state, which is different from the previous push and pop implementation separately. All routing switching operations are represented by state. At the same time, all routes are encapsulated as _RouteEntry. It has implementations about Route operations, but they are all divided into relatively small units and are executed by state.
State is an enumeration with progressive relationships. Each _RouteEntry has a variable to store the current state. In _flushHistoryUpdates, all _RouteEntry will be traversed and processed according to their current state. At the same time, after the processing is completed, their state will be switched and other processing will be performed. This is obvious. After all the routes are processed together, the entire process will become clearer and can be reused to a large extent. For example, push and pushReplacement operations, which were previously implemented separately in two methods, but now they can be processed separately. The only difference is that the latter will have one more remove operation than the former.
About the processing steps of _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 () { // ... } 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 String routeName = lastEntry?.route?.settings?.name; if (routeName != _lastAnnouncedRouteName) { (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 ) (); (); } if (rearrangeOverlay) overlay?.rearrange(_allRouteOverlayEntries); }
The above is the whole process of _flushHistoryUpdates in addition to state processing. First, it will traverse the entire route list and perform different processing according to the state. However, generally only one or two of them can be processed at the top level, and most of the rest will be skipped directly. After processing, call _flushRouteAnnouncement to link between routes, such as animation linkage, etc.
void _flushRouteAnnouncement() { int index = _history.length - 1; while (index >= 0) { final _RouteEntry entry = _history[index]; if (!) { index -= 1; continue; } final _RouteEntry next = _getRouteAfter(index + 1, _RouteEntry.suitableForTransitionAnimationPredicate); if (next?.route != ) { if ((next?.route)) { (next?.route); } = next?.route; } final _RouteEntry previous = _getRouteBefore(index - 1, _RouteEntry.suitableForTransitionAnimationPredicate); if (previous?.route != ) { (previous?.route); = previous?.route; } index -= 1; } }
Its implementation is also clear. For each _RouteEntry, you can make connections by calling didChangeNext and didChangePrevious. For example, bind the secondaryAnimation of the current Route in didChangeNext and the animation of the next route for animation. For example, get the title of the previous route in didChangePrevious. This can be used to display the title of the previous page by the back button in CupertinoNavigationBar.
Then call maybeNotifyRouteChange to issue a notification, specifying the Route that is currently in the presentation state.
Finally, traverse toBeDisposed to execute the destruction of _RouteEntry. This list will save the _RouteEntry that needs to be removed during the above loop processing. By calling the OverlayEntry remove function (it will remove itself from Overlay) and the OverlayEntry dispose function (it will call the dispose of Route to release resources, such as the AnimationController destroyed in TransitionRoute).
Finally, let’s look at the processing of states. The following 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 adding, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial // 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 }
In essence, these states are divided into three categories: add (add directly when processing initialization), push (similar to add, but adds animation processing), pop (processing page removal), and remove (moving out of a certain page, there is no animation and no location restrictions compared to pop).
add
Adding routes is currently only used when applying initialization is to add initialization page, corresponding to initState in NavigatorState.
void initState() { (); for (final NavigatorObserver observer in ) { assert( == null); observer._navigator = this; } String initialRoute = ; if () { _history.addAll( ((Page<dynamic> page) => _RouteEntry( (context), initialState: _RouteLifecycle.add, )) ); } else { // If there is no page provided, we will need to provide default route // to initialize the navigator. initialRoute = initialRoute ?? ; } if (initialRoute != null) { _history.addAll( ( this, ?? ).map((Route<dynamic> route) => _RouteEntry( route, initialState: _RouteLifecycle.add, ), ), ); } _flushHistoryUpdates(); }
It converts all initial routes obtained from onGenerateInitialRoutes into _RouteEntry to join _history, and their status is _RouteLifecycle.add, and then calls _flushHistoryUpdates for processing.
void _flushHistoryUpdates({bool rearrangeOverlay = true}) { // ... while (index >= 0) { switch () { case _RouteLifecycle.add: assert(rearrangeOverlay); ( navigator: this, ); assert( == _RouteLifecycle.adding); continue; case _RouteLifecycle.adding: if (canRemoveOrAdd || next == null) { ( navigator: this, previous: previous?.route, previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route, isNewFirst: next == null ); assert( == _RouteLifecycle.idle); continue; } break; case _RouteLifecycle.idle: if (!seenTopActiveRoute && poppedRoute != null) (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; } // ... }
The add route mainly 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; (); assert(); currentState = _RouteLifecycle.adding; }
The install function can be regarded as a Route initialization function. For example, creating a ProxyAnimation in ModalRoute to manage the execution of some animations, creating an AnimationController for executing switching animations in TransitionRoute, and creating and inserting the OverlayEntry of the current Route in OverlayRoute. createOverlayEntries is used to create OverlayEntry, but now ModalRoute is
Iterable<OverlayEntry> createOverlayEntries() sync* { yield _modalBarrier = OverlayEntry(builder: _buildModalBarrier); yield OverlayEntry(builder: _buildModalScope, maintainState: maintainState); }
Each Route can generate two OverlayEntry, one is _buildModalBarrier, which can generate a barrier between two pages. We can use it to set a background color for the new page, and it also supports animation transitions. The other is _buildModalScope, which generates the real content of this page, with multiple layers of packaging on the outside, and the bottom layer is the widget created by WidgetBuilder.
Let's take a look at the implementation of the 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 = ( 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: == || // changedInternalState is called when this updates == , // dismissed is possible when doing a manual pop gesture child: barrier, ); }
ModalBarrier is a barrier between two Routes. It can represent the isolation of two Routes through color and intercept events. These can be configured. The function of IgnorePointer here is to not respond to the time when executing switching animations.
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: , isCurrent: , // _routeSetState is called if this updates canPop: , // _routeSetState is called if this updates child: Offstage( offstage: , // _routeSetState is called if this updates child: PageStorage( bucket: ._storageBucket, // immutable child: FocusScope( node: focusScopeNode, // immutable child: RepaintBoundary( child: AnimatedBuilder( animation: _listenable, // immutable builder: (BuildContext context, Widget child) { return ( context, , , IgnorePointer( ignoring: ?.status == , child: child, ), ); }, child: _page ??= RepaintBoundary( key: ._subtreeKey, // immutable child: Builder( builder: (BuildContext context) { return ( context, , , ); }, ), ), ), ), ), ), ), ); }
_ModalScope needs to host the user interface display. Its build function can be seen that there are many layers on the page defined by the user. You can see the general function layer by layer:
- _ModalScopeStatus, inherited from InheritedWidget, is used to provide data to the underlying nodes
- Offstage, you can control whether to draw through the offstage variable
- PageStorage, which provides a storage strategy, that is, PageStorageBucket, this class can bind specific data to a BuildContext, support writing and reading, and can be used for state storage of a widget, etc.
- FocusScope is used for focus management. Generally, only the control that obtains the focus can receive key information, etc.
- RepaintBoundary, controls the redraw range, intended to reduce unnecessary redraw
- AnimatedBuilder, animation control Widget, will rebuild according to animation
- , it can have different implementations in different Routes. For example, the default implementation of Android is to gradually enter from bottom to top, the default implementation of ios is to slide from right to left, and you can also implement custom switching animations through custom Route or custom ThemeData. Another point to be explained is that the animation in Route is divided into animation and secondaryAnimation, where animation defines its animation when pushing, and secondaryAnimation defines its own animation when pushing a new page. For example, in the ios style, the new page slides from right to left, and the previous page will also slide. At this time, the animation that controls the sliding of the previous page is secondaryAnimation
- IgnorePointer, also used for page switching animation execution, prohibiting user operations
- RepaintBoundary, the consideration here should be to consider that there is an animation execution on the upper layer, so here is a package to avoid fixed content repainting.
- Builder, the only function of Builder should be to provide BuildContext. Although each build function has a BuildContext parameter, this is the current Widget, not directly under the superior. This may be a bit abstract. For example, the following buildPage needs to use BuildContext as a parameter. If it needs to use the context's ancestorStateOfType, it is actually looking up from _ModalScopeState, rather than from Builder.
- , this function uses Route's WidgetBuilder to create the user interface. Of course, different Routes may be wrapped again here
The above is the layout nesting down from Overlay (that is, Overlay is not so reasonable, but omit the middle _Theatre, etc.) in a page. After the new OverlayEntry is created, they will be passed to Overlay, and during this process, the setState function of Overlay will be called to request redrawing, and the switching between the old and new pages in Overlay will be implemented.
The above is the entire process of install. After execution, set currentState to add and return.
There is one thing to note here. The while loop will traverse all _RouteEntry from top to bottom. However, when a continuous operation has not been completed, it will not execute the next _RouteEntry. Its implementation lies in the continue keyword in the code. This keyword will directly return to execute the next loop, but does not update the current _RouteEntry, so the actual processing is still the same route. This is generally used when the state of _RouteEntry changes and requires continuous processing. Therefore, for add, the add code block will be executed immediately after execution, that is, didAdd.
void didAdd({ @required NavigatorState navigator, @required bool isNewFirst, @required Route<dynamic> previous, @required Route<dynamic> previousPresent }) { (); currentState = _RouteLifecycle.idle; if (isNewFirst) { (null); } for (final NavigatorObserver observer in ) (route, previousPresent); }
Route's didAdd function means that the route has been added and it will do some finalization processing, such as updating the AnimationController value to the maximum in TransitionRoute, and setting transparency, etc. didAdd then sets the status to idle and calls didPush for all listeners. idle means that a _RouteEntry has been processed. Only pop, replace and other operations will need to be reprocessed. The add process can 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 ; }
The push process is to encapsulate Route into _RouteEntry and add it to _history and call _flushHistoryUpdates. Its initial state is pushed and returned at the end. This is a Future object that can be used to receive the result of the new page from the previous page. This value is passed when the current route pop is popped.
void _flushHistoryUpdates({bool rearrangeOverlay = true}) { // ... while (index >= 0) { switch () { // ... case _RouteLifecycle.push: case _RouteLifecycle.pushReplace: case _RouteLifecycle.replace: assert(rearrangeOverlay); ( navigator: this, previous: previous?.route, previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route, isNewFirst: next == null, ); assert( != _RouteLifecycle.push); assert( != _RouteLifecycle.pushReplace); assert( != _RouteLifecycle.replace); if ( == _RouteLifecycle.idle) { continue; } break; case _RouteLifecycle.pushing: // Will exit this state when animation completes. if (!seenTopActiveRoute && poppedRoute != null) (poppedRoute); seenTopActiveRoute = true; break; case _RouteLifecycle.idle: if (!seenTopActiveRoute && poppedRoute != null) (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; } // ... }
Here, push, pushReplace, and replace are all classified into one category. It will call handlePush first. This function actually contains the functions of handleAdd and didAdd in the add process, such as calling install and didPush. The difference is that push/push will have a transition process, that is, first execute the switching animation, at this time, its state will become pushing, and when the animation is executed, it will cut to the idle state and call _flushHistoryUpdates to update, while replace directly calls didReplace to complete the page replacement. From here, this should not be an animation transition. The same is true afterwards, call the notification function.
pop
The process of pop is different from the above two, and it also has some operations in it:
void pop<T extends Object>([ T result ]) { assert(!_debugLocked); assert(() { _debugLocked = true; return true; }()); final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate); if () { if ((, result)) = _RouteLifecycle.pop; } else { <T>(result); } if ( == _RouteLifecycle.pop) { // Flush the history if the route actually wants to be popped (the pop // wasn't handled internally). _flushHistoryUpdates(rearrangeOverlay: false); assert(._popCompleter.isCompleted); } assert(() { _debugLocked = false; return true; }()); _afterNavigation<dynamic>(); }
It is to call the pop of _RouteEntry. In this function, it will call the didPop of Route to complete the transfer of the return value, move out the animation to start, etc. But in OverlayRoute:
bool didPop(T result) { final bool returnValue = (result); assert(returnValue); if (finishedWhenPopped) (this); return returnValue; }
The call to finalizeRoute needs to rely on the value of finishedWhenPopped. This value can be modified in a subclass. For example, in TransitionRoute, it is false, and the understanding is very simple. After executing didPop in TransitionRoute, you cannot directly destroy the Route. Instead, you must first execute the moving animation. If you do not need to execute the animation, you can call it directly. Otherwise, you will execute it before executing the animation state. This is achieved by monitoring the animation state, in TransitionRoute.
void finalizeRoute(Route<dynamic> route) { // FinalizeRoute may have been called while we were already locked as a // responds to (). 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 () { // We were called synchronously from (), but didn't process // the pop yet. Let's do that now before finalizing. = _RouteLifecycle.pop; _flushHistoryUpdates(rearrangeOverlay: false); } assert( != _RouteLifecycle.pop); (); _flushHistoryUpdates(rearrangeOverlay: false); assert(() { _debugLocked = wasDebugLocked; return true; }()); }
In finalizeRoute, it will determine whether it is in the pop process. If so, it means that the finalizeRoute is called directly at this moment. Then you need to execute the pop state operation first, then execute the dispose operation, and switch the state to dispose for processing. If not, it means that when this function is called, it is the time when the animation is executed, then the pop state processing has been completed at this moment, so the pop processing step has been skipped, as above. Let’s take a look at the process of pop.
void _flushHistoryUpdates({bool rearrangeOverlay = true}) { // ... while (index >= 0) { switch () { // ... case _RouteLifecycle.pop: if (!seenTopActiveRoute) { if (poppedRoute != null) (poppedRoute); poppedRoute = ; } ( navigator: this, previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route, ); assert( == _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. (_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; } // ... }
handlePop switches the state to popping (animation execution process), and then issues a notification, but the popping state is not processed because this is a transition state. After the animation is executed, it will automatically switch to the dispose state. Similarly, the pushing state above is also the dispose branch. In the dispose branch, _RouteEntry is removed from _history and added to toBeDisposed, and then destroyed uniformly after the traversal is completed.
remove
The logic of remove is to first find a _RouteEntry that is consistent with the passed in from _history, set its status to remvoe, and then call _flushHistoryUpdates.
void _flushHistoryUpdates({bool rearrangeOverlay = true}) { // ... while (index >= 0) { switch () { // ... case _RouteLifecycle.remove: if (!seenTopActiveRoute) { if (poppedRoute != null) (poppedRoute); poppedRoute = null; } ( navigator: this, previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route, ); assert( == _RouteLifecycle.removing); continue; case _RouteLifecycle.removing: if (!canRemoveOrAdd && next != null) { // We aren't allowed to remove this route yet. break; } = _RouteLifecycle.dispose; continue; case _RouteLifecycle.dispose: // Delay disposal until didChangeNext/didChangePrevious have been sent. (_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; } // ... }
First, handleRemoval will be called, notification will be called, and the state will be switched to removal. In the removal stage, the state will be cut to dispose, and then it will be added to BeDisposed. Therefore, there is no animation involved in the entire process. It is generally only used to move out of pages that are not displayed. Otherwise, pop is recommended.
Summarize
The above is the implementation principle of the routing mechanism. As a whole, the most refreshing thing is the addition of state management. By dividing the in and out of a page into different states, the complexity of the code can be effectively reduced. However, from the current results, the execution of this process is not refined enough, such as the division of states is not reasonable enough. Judging from the design of these states, add/push/pop has corresponding ing forms indicating that it is being executed, but I don’t see the need for adding for the time being. In addition, I feel that there are still some problems in the organization of the code. For example, handleAdd and handPush actually have a lot of code duplication. I don’t know if this part will be optimized in the future.
Another thing that seems to be inadequate is that the _routeNamed function is not open to the public, and not all routing operations provide a wrapper with name as parameter, such as removeRoute, which cannot be called very conveniently in this case.
This is the end of this article about the implementation of the flutter routing mechanism. For more related flutter routing mechanism content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!