diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart index b57d352293..c81cae0505 100644 --- a/packages/flutter/lib/src/widgets/navigator.dart +++ b/packages/flutter/lib/src/widgets/navigator.dart @@ -36,11 +36,6 @@ import 'ticker_provider.dart'; /// * [Navigator], which is where all the [Route]s end up. typedef RouteFactory = Route Function(RouteSettings settings); -/// Creates a route for the given context and route settings. -/// -/// Used by [CustomBuilderPage.routeBuilder]. -typedef RouteBuilder = Route Function(BuildContext context, RouteSettings settings); - /// Creates a series of one or more routes. /// /// Used by [Navigator.onGenerateInitialRoutes]. @@ -55,15 +50,6 @@ typedef RoutePredicate = bool Function(Route route); /// [ModalRoute.removeScopedWillPopCallback], and [WillPopScope]. typedef WillPopCallback = Future Function(); -/// Signature for the [Navigator.onPopPage] callback. -/// -/// This callback must call [Route.didPop] on the specified route and must -/// properly update the pages list the next time it is passed into -/// [Navigator.pages] so that it no longer includes the corresponding [Page]. -/// (Otherwise, the page will be interpreted as a new page to show when the -/// [Navigator.pages] list is next updated.) -typedef PopPageCallback = bool Function(Route route, dynamic result); - /// Indicates whether the current route should be popped. /// /// Used as the return value for [Route.willPop]. @@ -104,13 +90,6 @@ enum RoutePopDisposition { /// See [MaterialPageRoute] for a route that replaces the entire screen with a /// platform-adaptive transition. /// -/// A route can belong to a page if the [settings] are a subclass of [Page]. A -/// page-based route, as opposite to pageless route, is created from -/// [Page.createRoute] during [Navigator.pages] updates. The page associated -/// with this route may change during the lifetime of the route. If the -/// [Navigator] updates the page of this route, it calls [changedInternalState] -/// to notify the route that the page has been updated. -/// /// The type argument `T` is the route's return type, as used by /// [currentResult], [popped], and [didPop]. The type `void` may be used if the /// route does not return a value. @@ -119,7 +98,7 @@ abstract class Route { /// /// If the [settings] are not provided, an empty [RouteSettings] object is /// used instead. - Route({ RouteSettings settings }) : _settings = settings ?? const RouteSettings(); + Route({ RouteSettings settings }) : settings = settings ?? const RouteSettings(); /// The navigator that the route is in, if any. NavigatorState get navigator => _navigator; @@ -128,26 +107,7 @@ abstract class Route { /// The settings for this route. /// /// See [RouteSettings] for details. - /// - /// The settings can change during the route's lifetime. If the settings - /// change, the route's overlays will be marked dirty (see - /// [changedInternalState]). - /// - /// If the route is created from a [Page] in the [Navigator.pages] list, then - /// this will be a [Page] subclass, and it will be updated each time its - /// corresponding [Page] in the [Navigator.pages] has changed. Once the - /// [Route] is removed from the history, this value stops updating (and - /// remains with its last value). - RouteSettings get settings => _settings; - RouteSettings _settings; - - void _updateSettings(RouteSettings newSettings) { - assert(newSettings != null); - if (_settings != newSettings) { - _settings = newSettings; - changedInternalState(); - } - } + final RouteSettings settings; /// The overlay entries of this route. /// @@ -287,6 +247,7 @@ abstract class Route { /// /// See [popped], [didComplete], and [currentResult] for a discussion of the /// `result` argument. + @protected @mustCallSuper bool didPop(T result) { didComplete(result); @@ -486,82 +447,6 @@ class RouteSettings { String toString() => '${objectRuntimeType(this, 'RouteSettings')}("$name", $arguments)'; } -/// Describes the configuration of a [Route]. -/// -/// The type argument `T` is the corresponding [Route]'s return type, as -/// used by [Route.currentResult], [Route.popped], and [Route.didPop]. -/// -/// See also: -/// -/// * [Navigator.pages], which accepts a list of [Page]s and updates its routes -/// history. -/// * [CustomBuilderPage], a [Page] subclass that provides the API to build a -/// customized route. -abstract class Page extends RouteSettings { - /// Creates a page and initializes [key] for subclasses. - /// - /// The [arguments] argument must not be null. - const Page({ - this.key, - String name, - Object arguments, - }) : super(name: name, arguments: arguments); - - /// The key associated with this page. - /// - /// This key will be used for comparing pages in [canUpdate]. - final LocalKey key; - - /// Whether this page can be updated with the [other] page. - /// - /// Two pages are consider updatable if they have same the [runtimeType] and - /// [key]. - bool canUpdate(Page other) { - return other.runtimeType == runtimeType && - other.key == key; - } - - /// Creates the [Route] that corresponds to this page. - /// - /// The created [Route] must have its [Route.settings] property set to this [Page]. - Route createRoute(BuildContext context); - - @override - String toString() => '${objectRuntimeType(this, 'Page')}("$name", $key, $arguments)'; -} - -/// A [Page] that builds a customized [Route] based on the [routeBuilder]. -/// -/// The type argument `T` is the corresponding [Route]'s return type, as -/// used by [Route.currentResult], [Route.popped], and [Route.didPop]. -class CustomBuilderPage extends Page { - /// Creates a page with a custom route builder. - /// - /// Use [routeBuilder] to specify the route that will be created from this - /// page. - const CustomBuilderPage({ - @required LocalKey key, - @required this.routeBuilder, - String name, - Object arguments, - }) : assert(key != null), - assert(routeBuilder != null), - super(key: key, name: name, arguments: arguments); - - /// A builder that will be called during [createRoute] to create a [Route]. - /// - /// The routes returned from this builder must have their settings equal to - /// the input `settings`. - final RouteBuilder routeBuilder; - - @override - Route createRoute(BuildContext context) { - final Route route = routeBuilder(context, this); - assert(route.settings == this); - return route; - } -} - /// An interface for observing the behavior of a [Navigator]. class NavigatorObserver { /// The navigator that the observer is observing, if any. @@ -606,336 +491,6 @@ class NavigatorObserver { void didStopUserGesture() { } } -/// A [Route] wrapper interface that can be staged for [TransitionDelegate] to -/// decide how its underlying [Route] should transition on or off screen. -abstract class RouteTransitionRecord { - /// Retrieves the wrapped [Route]. - Route get route; - - /// Whether this route is entering the screen. - /// - /// If this property is true, this route requires an explicit decision on how - /// to transition into the screen. Such a decision should be made in the - /// [TransitionDelegate.resolve]. - bool get isEntering; - - bool _debugWaitingForExitDecision = false; - - /// Marks the [route] to be pushed with transition. - /// - /// During [TransitionDelegate.resolve], this can be called on an entering - /// route (where [RouteTransitionRecord.isEntering] is true) in indicate that the - /// route should be pushed onto the [Navigator] with an animated transition. - void markForPush(); - - /// Marks the [route] to be added without transition. - /// - /// During [TransitionDelegate.resolve], this can be called on an entering - /// route (where [RouteTransitionRecord.isEntering] is true) in indicate that the - /// route should be added onto the [Navigator] without an animated transition. - void markForAdd(); - - /// Marks the [route] to be popped with transition. - /// - /// During [TransitionDelegate.resolve], this can be called on an exiting - /// route to indicate that the route should be popped off the [Navigator] with - /// an animated transition. - void markForPop([dynamic result]); - - /// Marks the [route] to be completed without transition. - /// - /// During [TransitionDelegate.resolve], this can be called on an exiting - /// route to indicate that the route should be completed with the provided - /// result and removed from the [Navigator] without an animated transition. - void markForComplete([dynamic result]); - - /// Marks the [route] to be removed without transition. - /// - /// During [TransitionDelegate.resolve], this can be called on an exiting - /// route to indicate that the route should be removed from the [Navigator] - /// without completing and without an animated transition. - void markForRemove(); -} - -/// The delegate that decides how pages added and removed from [Navigator.pages] -/// transition in or out of the screen. -/// -/// This abstract class implements the API to be called by [Navigator] when it -/// requires explicit decisions on how the routes transition on or off the screen. -/// -/// To make route transition decisions, subclass must implement [resolve]. -/// -/// {@tool sample --template=freeform} -/// The following example demonstrates how to implement a subclass that always -/// removes or adds routes without animated transitions and puts the removed -/// routes at the top of the list. -/// -/// ```dart imports -/// import 'package:flutter/widgets.dart'; -/// ``` -/// -/// ```dart -/// class NoAnimationTransitionDelegate extends TransitionDelegate { -/// @override -/// Iterable resolve({ -/// List newPageRouteHistory, -/// Map locationToExitingPageRoute, -/// Map> pageRouteToPagelessRoutes, -/// }) { -/// final List results = []; -/// -/// for (final RouteTransitionRecord pageRoute in newPageRouteHistory) { -/// if (pageRoute.isEntering) { -/// pageRoute.markForAdd(); -/// } -/// results.add(pageRoute); -/// -/// } -/// for (final RouteTransitionRecord exitingPageRoute in locationToExitingPageRoute.values) { -/// exitingPageRoute.markForRemove(); -/// final List pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute]; -/// if (pagelessRoutes != null) { -/// for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) { -/// pagelessRoute.markForRemove(); -/// } -/// } -/// results.add(exitingPageRoute); -/// -/// } -/// return results; -/// } -/// } -/// -/// ``` -/// {@end-tool} -/// -/// See also: -/// -/// * [Navigator.transitionDelegate], which uses this class to make route -/// transition decisions. -/// * [DefaultTransitionDelegate], which implements the default way to decide -/// how routes transition in or out of the screen. -abstract class TransitionDelegate { - /// Creates a delegate and enables subclass to create a constant class. - const TransitionDelegate(); - - Iterable _transition({ - List newPageRouteHistory, - Map locationToExitingPageRoute, - Map> pageRouteToPagelessRoutes, - }) { - final Iterable results = resolve( - newPageRouteHistory: newPageRouteHistory, - locationToExitingPageRoute: locationToExitingPageRoute, - pageRouteToPagelessRoutes: pageRouteToPagelessRoutes, - ); - // Verifies the integrity after the decisions have been made. - // - // Here are the rules: - // - All the entering routes in newPageRouteHistory must either be pushed or - // added. - // - All the exiting routes in locationToExitingPageRoute must either be - // popped, completed or removed. - // - All the pageless routes that belong to exiting routes must either be - // popped, completed or removed. - // - All the entering routes in the result must preserve the same order as - // the entering routes in newPageRouteHistory, and the result must contain - // all exiting routes. - // ex: - // - // newPageRouteHistory = [A, B, C] - // - // locationToExitingPageRoute = {A -> D, C -> E} - // - // results = [A, B ,C ,D ,E] is valid - // results = [D, A, B ,C ,E] is also valid because exiting route can be - // inserted in any place - // - // results = [B, A, C ,D ,E] is invalid because B must be after A. - // results = [A, B, C ,E] is invalid because results must include D. - assert(() { - final List resultsToVerify = results.toList(growable: false); - final Set exitingPageRoutes = locationToExitingPageRoute.values.toSet(); - // Firstly, verifies all exiting routes have been marked. - for (final RouteTransitionRecord exitingPageRoute in exitingPageRoutes) { - assert(!exitingPageRoute._debugWaitingForExitDecision); - if (pageRouteToPagelessRoutes.containsKey(exitingPageRoute)) { - for (final RouteTransitionRecord pagelessRoute in pageRouteToPagelessRoutes[exitingPageRoute]) { - assert(!pagelessRoute._debugWaitingForExitDecision); - } - } - } - // Secondly, verifies the order of results matches the newPageRouteHistory - // and contains all the exiting routes. - int indexOfNextRouteInNewHistory = 0; - - for (final _RouteEntry routeEntry in resultsToVerify.cast<_RouteEntry>()) { - assert(routeEntry != null); - assert(!routeEntry.isEntering && !routeEntry._debugWaitingForExitDecision); - if ( - indexOfNextRouteInNewHistory >= newPageRouteHistory.length || - routeEntry != newPageRouteHistory[indexOfNextRouteInNewHistory] - ) { - assert(exitingPageRoutes.contains(routeEntry)); - exitingPageRoutes.remove(routeEntry); - } else { - indexOfNextRouteInNewHistory += 1; - } - } - - assert( - indexOfNextRouteInNewHistory == newPageRouteHistory.length && - exitingPageRoutes.isEmpty - ); - return true; - }()); - - return results; - } - - /// A method that will be called by the [Navigator] to decide how routes - /// transition in or out of the screen when [Navigator.pages] is updated. - /// - /// The `newPageRouteHistory` list contains all page-based routes in the order - /// that will be on the [Navigator]'s history stack after this update - /// completes. If a route in `newPageRouteHistory` has its - /// [RouteTransitionRecord.isEntering] set to true, this route requires explicit - /// decision on how it should transition onto the Navigator. To make a - /// decision, call [RouteTransitionRecord.markForPush] or - /// [RouteTransitionRecord.markForAdd]. - /// - /// The `locationToExitingPageRoute` contains the pages-based routes that - /// are removed from the routes history after page update and require explicit - /// decision on how to transition off the screen. This map records page-based - /// routes to be removed with the location of the route in the original route - /// history before the update. The keys are the locations represented by the - /// page-based routes that are directly below the removed routes, and the value - /// are the page-based routes to be removed. The location is null if the route - /// to be removed is the bottom most route. To make a decision for a removed - /// route, call [RouteTransitionRecord.markForPop], - /// [RouteTransitionRecord.markForComplete] or - /// [RouteTransitionRecord.markForRemove]. - /// - /// The `pageRouteToPagelessRoutes` records the page-based routes and their - /// associated pageless routes. If a page-based route is to be removed, its - /// associated pageless routes also require explicit decisions on how to - /// transition off the screen. - /// - /// Once all the decisions have been made, this method must merge the removed - /// routes and the `newPageRouteHistory` and return the merged result. The - /// order in the result will be the order the [Navigator] uses for updating - /// the route history. The return list must preserve the same order of routes - /// in `newPageRouteHistory`. The removed routes, however, can be inserted - /// into the return list freely as long as all of them are included. - /// - /// For example, consider the following case. - /// - /// newPageRouteHistory = [A, B, C] - /// - /// locationToExitingPageRoute = {A -> D, C -> E} - /// - /// The following outputs are valid. - /// - /// result = [A, B ,C ,D ,E] is valid - /// result = [D, A, B ,C ,E] is also valid because exiting route can be - /// inserted in any place - /// - /// The following outputs are invalid. - /// - /// result = [B, A, C ,D ,E] is invalid because B must be after A. - /// result = [A, B, C ,E] is invalid because results must include D. - /// - /// See also: - /// - /// * [RouteTransitionRecord.markForPush], which makes route enter the screen - /// with an animated transition. - /// * [RouteTransitionRecord.markForAdd], which makes route enter the screen - /// without an animated transition. - /// * [RouteTransitionRecord.markForPop], which makes route exit the screen - /// with an animated transition. - /// * [RouteTransitionRecord.markForRemove], which does not complete the - /// route and makes it exit the screen without an animated transition. - /// * [RouteTransitionRecord.markForComplete], which completes the route and - /// makes it exit the screen without an animated transition. - /// * [DefaultTransitionDelegate.resolve], which implements the default way - /// to decide how routes transition in or out of the screen. - Iterable resolve({ - List newPageRouteHistory, - Map locationToExitingPageRoute, - Map> pageRouteToPagelessRoutes, - }); -} - -/// The default implementation of [TransitionDelegate] that the [Navigator] will -/// use if its [Navigator.transitionDelegate] is not specified. -/// -/// This transition delegate follows two rules. Firstly, all the entering routes -/// are placed on top of the exiting routes if they are at the same location. -/// Secondly, the top most route will always transition with an animated transition. -/// All the other routes below will either be completed with -/// [Route.currentResult] or added without an animated transition. -class DefaultTransitionDelegate extends TransitionDelegate { - /// Creates a default transition delegate. - const DefaultTransitionDelegate() : super(); - - @override - Iterable resolve({ - List newPageRouteHistory, - Map locationToExitingPageRoute, - Map> pageRouteToPagelessRoutes, - }) { - final List results = []; - // This method will handle the exiting route and its corresponding pageless - // route at this location. It will also recursively check if there is any - // other exiting routes above it and handle them accordingly. - void handleExitingRoute(RouteTransitionRecord location, bool isLast) { - final RouteTransitionRecord exitingPageRoute = locationToExitingPageRoute[location]; - if (exitingPageRoute == null) - return; - assert(exitingPageRoute._debugWaitingForExitDecision); - final bool hasPagelessRoute = pageRouteToPagelessRoutes.containsKey(exitingPageRoute); - final bool isLastExitingPageRoute = isLast && !locationToExitingPageRoute.containsKey(exitingPageRoute); - if (isLastExitingPageRoute && !hasPagelessRoute) { - exitingPageRoute.markForPop(exitingPageRoute.route.currentResult); - } else { - exitingPageRoute.markForComplete(exitingPageRoute.route.currentResult); - } - results.add(exitingPageRoute); - - if (hasPagelessRoute) { - final List pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute]; - for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) { - assert(pagelessRoute._debugWaitingForExitDecision); - if (isLastExitingPageRoute && pagelessRoute == pagelessRoutes.last) { - pagelessRoute.markForPop(pagelessRoute.route.currentResult); - } else { - pagelessRoute.markForComplete(pagelessRoute.route.currentResult); - } - } - } - // It is possible there is another exiting route above this exitingPageRoute. - handleExitingRoute(exitingPageRoute, isLast); - } - - // Handles exiting route in the beginning of list. - handleExitingRoute(null, newPageRouteHistory.isEmpty); - - for (final RouteTransitionRecord pageRoute in newPageRouteHistory) { - final bool isLastIteration = newPageRouteHistory.last == pageRoute; - if (pageRoute.isEntering) { - if (!locationToExitingPageRoute.containsKey(pageRoute) && isLastIteration) { - pageRoute.markForPush(); - } else { - pageRoute.markForAdd(); - } - } - results.add(pageRoute); - handleExitingRoute(pageRoute, isLastIteration); - } - return results; - } -} - /// A widget that manages a set of child widgets with a stack discipline. /// /// Many apps have a navigator near the top of their widget hierarchy in order @@ -950,9 +505,8 @@ class DefaultTransitionDelegate extends TransitionDelegate { /// Mobile apps typically reveal their contents via full-screen elements /// called "screens" or "pages". In Flutter these elements are called /// routes and they're managed by a [Navigator] widget. The navigator -/// manages a stack of [Route] objects and provides two ways for managing -/// the stack, the declarative API [Navigator.pages] or imperative API -/// [Navigator.push] and [Navigator.pop]. +/// manages a stack of [Route] objects and provides methods for managing +/// the stack, like [Navigator.push] and [Navigator.pop]. /// /// When your user interface fits this paradigm of a stack, where the user /// should be able to _navigate_ back to an earlier element in the stack, @@ -964,21 +518,6 @@ class DefaultTransitionDelegate extends TransitionDelegate { /// used in the [Scaffold.appBar] property) can automatically add a back /// button for user navigation. /// -/// ## Using the Pages API -/// -/// The [Navigator] will convert its [Navigator.pages] into a stack of [Route]s -/// if it is provided. A change in [Navigator.pages] will trigger an update to -/// the stack of [Route]s. The [Navigator] will update its routes to match the -/// new configuration of its [Navigator.pages]. To use this API, one can use -/// [CustomBuilderPage] or create a [Page] subclass and defines a list of -/// [Page]s for [Navigator.pages]. A [Navigator.onPopPage] callback is also -/// required to properly clean up the input pages in case of a pop. -/// -/// By Default, the [Navigator] will use [DefaultTransitionDelegate] to decide -/// how routes transition in or out of the screen. To customize it, define a -/// [TransitionDelegate] subclass and provide it to the -/// [Navigator.transitionDelegate]. -/// /// ### Displaying a full-screen route /// /// Although you can create a navigator directly, it's most common to use the @@ -1194,7 +733,7 @@ class DefaultTransitionDelegate extends TransitionDelegate { /// ``` /// /// ```dart main -/// void main() => runApp(MyApp()); +/// void main() => runApp(new MyApp()); /// ``` /// /// ```dart @@ -1320,77 +859,17 @@ class DefaultTransitionDelegate extends TransitionDelegate { class Navigator extends StatefulWidget { /// Creates a widget that maintains a stack-based history of child widgets. /// - /// The [onGenerateRoute], [pages], [onGenerateInitialRoutes], - /// [transitionDelegate], [observers] arguments must not be null. - /// - /// If the [pages] is not empty, the [onPopPage] must not be null. + /// The [onGenerateRoute] argument must not be null. const Navigator({ Key key, - this.pages = const >[], - this.onPopPage, this.initialRoute, this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes, this.onGenerateRoute, this.onUnknownRoute, - this.transitionDelegate = const DefaultTransitionDelegate(), this.observers = const [], - }) : assert(pages != null), - assert(onGenerateInitialRoutes != null), - assert(transitionDelegate != null), - assert(observers != null), + }) : assert(onGenerateInitialRoutes != null), super(key: key); - /// The list of pages with which to populate the history. - /// - /// Pages are turned into routes using [Page.createRoute] in a manner - /// analogous to how [Widget]s are turned into [Element]s (and [State]s or - /// [RenderObject]s) using [Widget.createElement] (and - /// [StatefulWidget.createState] or [RenderObjectWidget.createRenderObject]). - /// - /// When this list is updated, the new list is compared to the previous - /// list and the set of routes is updated accordingly. - /// - /// Some [Route]s do not correspond to [Page] objects, namely, those that are - /// added to the history using the [Navigator] API ([push] and friends). A - /// [Route] that does not correspond to a [Page] object is called a pageless - /// route and is tied to the [Route] that _does_ correspond to a [Page] object - /// that is below it in the history. - /// - /// Pages that are added or removed may be animated as controlled by the - /// [transitionDelegate]. If a page is removed that had other pageless routes - /// pushed on top of it using [push] and friends, those pageless routes are - /// also removed with or without animation as determined by the - /// [transitionDelegate]. - /// - /// To use this API, an [onPopPage] callback must also be provided to properly - /// clean up this list if a page has been popped. - /// - /// If [initialRoute] is non-null when the widget is first created, then - /// [onGenerateInitialRoutes] is used to generate routes that are above those - /// corresponding to [pages] in the initial history. - final List> pages; - - /// Called when [pop] is invoked but the current [Route] corresponds to a - /// [Page] found in the [pages] list. - /// - /// The `result` argument is the value with which the route is to complete - /// (e.g. the value returned from a dialog). - /// - /// This callback is responsible for calling [Route.didPop] and returning - /// whether this pop is successful. - /// - /// The [Navigator] widget should be rebuilt with a [pages] list that does not - /// contain the [Page] for the given [Route]. The next time the [pages] list - /// is updated, if the [Page] corresponding to this [Route] is still present, - /// it will be interpreted as a new route to display. - final PopPageCallback onPopPage; - - /// The delegate used for deciding how routes transition in or off the screen - /// during the [pages] updates. - /// - /// Defaults to [DefaultTransitionDelegate] if not specified, cannot be null. - final TransitionDelegate transitionDelegate; - /// The name of the first route to show. /// /// Defaults to [Navigator.defaultRouteName]. @@ -2201,14 +1680,6 @@ class Navigator extends StatefulWidget { // The _RouteLifecycle state machine (only goes down): // // [creation of a _RouteEntry] -// | -// + -// |\ -// | \ -// | staging -// | / -// |/ -// +-+----------+--+-------+ // / | | | // / | | | // / | | | @@ -2217,7 +1688,7 @@ class Navigator extends StatefulWidget { // pushReplace push* add* replace* // \ | | | // \ | | / -// +--pushing# adding / +// +--pushing# | / // \ / / // \ / / // idle--+-----+ @@ -2243,69 +1714,47 @@ class Navigator extends StatefulWidget { // route entry will exit that state. // # These states await futures or other events, then transition automatically. enum _RouteLifecycle { - staging, // we will wait for transition delegate to decide what to do with this route. - // - // routes that are present: - // + // routes that are and will be present: add, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages - adding, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages - // 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. + // routes that are but will not present: 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. + // routes that are not and will not present: 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 } typedef _RouteEntryPredicate = bool Function(_RouteEntry entry); -class _RouteEntry extends RouteTransitionRecord { +class _RouteEntry { _RouteEntry( this.route, { @required _RouteLifecycle initialState, }) : assert(route != null), assert(initialState != null), assert( - initialState == _RouteLifecycle.staging || initialState == _RouteLifecycle.add || initialState == _RouteLifecycle.push || initialState == _RouteLifecycle.pushReplace || initialState == _RouteLifecycle.replace ), - currentState = initialState; + currentState = initialState; // ignore: prefer_initializing_formals - @override final Route route; _RouteLifecycle currentState; + Route lastAnnouncedNextRoute; // last argument to Route.didChangeNext Route lastAnnouncedPreviousRoute; // last argument to Route.didChangePrevious Route lastAnnouncedPoppedNextRoute; // last argument to Route.didPopNext - Route lastAnnouncedNextRoute; // last argument to Route.didChangeNext - bool get hasPage => route.settings is Page; - - bool canUpdateFrom(Page page) { - if (currentState.index > _RouteLifecycle.idle.index) - return false; - if (!hasPage) - return false; - final Page routePage = route.settings as Page; - return page.canUpdate(routePage); - } - - void handleAdd({ @required NavigatorState navigator}) { + void handleAdd({ @required NavigatorState navigator, @required bool isNewFirst, @required Route previous, @required Route previousPresent }) { assert(currentState == _RouteLifecycle.add); assert(navigator != null); assert(navigator._debugLocked); @@ -2313,7 +1762,13 @@ class _RouteEntry extends RouteTransitionRecord { route._navigator = navigator; route.install(); assert(route.overlayEntries.isNotEmpty); - currentState = _RouteLifecycle.adding; + route.didAdd(); + currentState = _RouteLifecycle.idle; + if (isNewFirst) { + route.didChangeNext(null); + } + for (final NavigatorObserver observer in navigator.widget.observers) + observer.didPush(route, previousPresent); } void handlePush({ @required NavigatorState navigator, @required bool isNewFirst, @required Route previous, @required Route previousPresent }) { @@ -2383,16 +1838,6 @@ class _RouteEntry extends RouteTransitionRecord { bool doingPop = false; - void didAdd({ @required NavigatorState navigator, @required bool isNewFirst, @required Route previous, @required Route previousPresent }) { - route.didAdd(); - currentState = _RouteLifecycle.idle; - if (isNewFirst) { - route.didChangeNext(null); - } - for (final NavigatorObserver observer in navigator.widget.observers) - observer.didPush(route, previousPresent); - } - void pop(T result) { assert(isPresent); doingPop = true; @@ -2406,11 +1851,6 @@ class _RouteEntry extends RouteTransitionRecord { // Route is removed without being completed. void remove({ bool isReplaced = false }) { - assert( - !hasPage || _debugWaitingForExitDecision, - 'A page-based route cannot be completed using imperative api, provide a ' - 'new list without the corresponding Page to Navigator.pages instead. ' - ); if (currentState.index >= _RouteLifecycle.remove.index) return; assert(isPresent); @@ -2420,11 +1860,6 @@ class _RouteEntry extends RouteTransitionRecord { // Route completes with `result` and is removed. void complete(T result, { bool isReplaced = false }) { - assert( - !hasPage || _debugWaitingForExitDecision, - 'A page-based route cannot be completed using imperative api, provide a ' - 'new list without the corresponding Page to Navigator.pages instead. ' - ); if (currentState.index >= _RouteLifecycle.remove.index) return; assert(isPresent); @@ -2445,25 +1880,8 @@ class _RouteEntry extends RouteTransitionRecord { currentState = _RouteLifecycle.disposed; } - bool get willBePresent { - return currentState.index <= _RouteLifecycle.idle.index && - currentState.index >= _RouteLifecycle.add.index; - } - - bool get isPresent { - return currentState.index <= _RouteLifecycle.remove.index && - currentState.index >= _RouteLifecycle.add.index; - } - - bool get suitableForAnnouncement { - return currentState.index <= _RouteLifecycle.removing.index && - currentState.index >= _RouteLifecycle.push.index; - } - - bool get suitableForTransitionAnimation { - return currentState.index <= _RouteLifecycle.remove.index && - currentState.index >= _RouteLifecycle.push.index; - } + bool get willBePresent => currentState.index <= _RouteLifecycle.idle.index; + bool get isPresent => currentState.index <= _RouteLifecycle.remove.index; bool shouldAnnounceChangeToNext(Route nextRoute) { assert(nextRoute != lastAnnouncedNextRoute); @@ -2477,76 +1895,17 @@ class _RouteEntry extends RouteTransitionRecord { } static final _RouteEntryPredicate isPresentPredicate = (_RouteEntry entry) => entry.isPresent; - static final _RouteEntryPredicate suitableForTransitionAnimationPredicate = (_RouteEntry entry) => entry.suitableForTransitionAnimation; static final _RouteEntryPredicate willBePresentPredicate = (_RouteEntry entry) => entry.willBePresent; static _RouteEntryPredicate isRoutePredicate(Route route) { return (_RouteEntry entry) => entry.route == route; } - - @override - bool get isEntering => currentState == _RouteLifecycle.staging; - - @override - void markForPush() { - assert( - isEntering && !_debugWaitingForExitDecision, - 'This route cannot be marked for push. Either a decision has already been ' - 'made or it does not require an explicit decision on how to transition in.' - ); - currentState = _RouteLifecycle.push; - } - - @override - void markForAdd() { - assert( - isEntering && !_debugWaitingForExitDecision, - 'This route cannot be marked for add. Either a decision has already been ' - 'made or it does not require an explicit decision on how to transition in.' - ); - currentState = _RouteLifecycle.add; - } - - @override - void markForPop([dynamic result]) { - assert( - !isEntering && _debugWaitingForExitDecision, - 'This route cannot be marked for pop. Either a decision has already been ' - 'made or it does not require an explicit decision on how to transition out.' - ); - pop(result); - _debugWaitingForExitDecision = false; - } - - @override - void markForComplete([dynamic result]) { - assert( - !isEntering && _debugWaitingForExitDecision, - 'This route cannot be marked for complete. Either a decision has already ' - 'been made or it does not require an explicit decision on how to transition ' - 'out.' - ); - complete(result); - _debugWaitingForExitDecision = false; - } - - @override - void markForRemove() { - assert( - !isEntering && _debugWaitingForExitDecision, - 'This route cannot be marked for remove. Either a decision has already ' - 'been made or it does not require an explicit decision on how to transition ' - 'out.' - ); - remove(); - _debugWaitingForExitDecision = false; - } } /// The state for a [Navigator] widget. class NavigatorState extends State with TickerProviderStateMixin { final GlobalKey _overlayKey = GlobalKey(); - List<_RouteEntry> _history = <_RouteEntry>[]; + final List<_RouteEntry> _history = <_RouteEntry>[]; /// The [FocusScopeNode] for the [FocusScope] that encloses the routes. final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope'); @@ -2556,40 +1915,20 @@ class NavigatorState extends State with TickerProviderStateMixin { @override void initState() { super.initState(); - assert( - widget.pages.isEmpty || widget.onPopPage != null, - 'The Navigator.onPopPage must be provided to use the Navigator.pages API', - ); for (final NavigatorObserver observer in widget.observers) { assert(observer.navigator == null); observer._navigator = this; } - String initialRoute = widget.initialRoute; - if (widget.pages.isNotEmpty) { - _history.addAll( - widget.pages.map((Page page) => _RouteEntry( - page.createRoute(context), + // TODO(chunhtai): Uses pages after we add page api. + // https://github.com/flutter/flutter/issues/45938 + _history.addAll( + widget.onGenerateInitialRoutes(this, widget.initialRoute ?? Navigator.defaultRouteName) + .map((Route route) => _RouteEntry( + route, initialState: _RouteLifecycle.add, - )) - ); - } else { - // If there is no page provided, we will need to provide default route - // to initialize the navigator. - initialRoute = initialRoute ?? Navigator.defaultRouteName; - } - if (initialRoute != null) { - _history.addAll( - widget.onGenerateInitialRoutes( - this, - widget.initialRoute ?? Navigator.defaultRouteName - ).map((Route route) => - _RouteEntry( - route, - initialState: _RouteLifecycle.add, - ), ), - ); - } + ), + ); assert(!_debugLocked); assert(() { _debugLocked = true; return true; }()); _flushHistoryUpdates(); @@ -2599,10 +1938,6 @@ class NavigatorState extends State with TickerProviderStateMixin { @override void didUpdateWidget(Navigator oldWidget) { super.didUpdateWidget(oldWidget); - assert( - widget.pages.isEmpty || widget.onPopPage != null, - 'The Navigator.onPopPage must be provided to use the Navigator.pages API', - ); if (oldWidget.observers != widget.observers) { for (final NavigatorObserver observer in oldWidget.observers) observer._navigator = null; @@ -2611,31 +1946,10 @@ class NavigatorState extends State with TickerProviderStateMixin { observer._navigator = this; } } - if (oldWidget.pages != widget.pages) { - assert( - widget.pages.isNotEmpty, - 'To use the Navigator.pages, there must be at least one page in the list.' - ); - _updatePages(); - } - for (final _RouteEntry entry in _history) entry.route.changedExternalState(); } - void _debugCheckDuplicatedPageKeys() { - assert((){ - final Set keyReservation = {}; - for (final Page page in widget.pages) { - if (page.key != null) { - assert(!keyReservation.contains(page.key)); - keyReservation.add(page.key); - } - } - return true; - }()); - } - @override void dispose() { assert(!_debugLocked); @@ -2663,276 +1977,8 @@ class NavigatorState extends State with TickerProviderStateMixin { String _lastAnnouncedRouteName; - bool _debugUpdatingPage = false; - void _updatePages() { - assert(() { - assert(!_debugUpdatingPage); - _debugCheckDuplicatedPageKeys(); - _debugUpdatingPage = true; - return true; - }()); - - // This attempts to diff the new pages list (widget.pages) with - // the old _RouteEntry[s] list (_history), and produces a new list of - // _RouteEntry[s] to be the new list of _history. This method roughly - // follows the same outline of RenderObjectElement.updateChildren. - // - // The cases it tries to optimize for are: - // - the old list is empty - // - All the pages in the new list can match the page-based routes in the old - // list, and their orders are the same. - // - there is an insertion or removal of one or more page-based route in - // only one place in the list - // If a page-based route with a key is in both lists, it will be synced. - // Page-based routes without keys might be synced but there is no guarantee. - - // The general approach is to sync the entire new list backwards, as follows: - // 1. Walk the lists from the bottom, syncing nodes, and record pageless routes, - // until you no longer have matching nodes. - // 2. Walk the lists from the top, without syncing nodes, until you no - // longer have matching nodes. We'll sync these nodes at the end. We - // don't sync them now because we want to sync all the nodes in order - // from beginning to end. - // At this point we narrowed the old and new lists to the point - // where the nodes no longer match. - // 3. Walk the narrowed part of the old list to get the list of - // keys. - // 4. Walk the narrowed part of the new list forwards: - // * Create a new _RouteEntry for non-keyed items and record them for - // transitionDelegate. - // * Sync keyed items with the source if it exists. - // 5. Walk the narrowed part of the old list again to records the - // _RouteEntry[s], as well as pageless routes, needed to be removed for - // transitionDelegate. - // 5. Walk the top of the list again, syncing the nodes and recording - // pageless routes. - // 6. Use transitionDelegate for explicit decisions on how _RouteEntry[s] - // transition in or off the screens. - // 7. Fill pageless routes back into the new history. - - bool needsExplicitDecision = false; - int newPagesBottom = 0; - int oldEntriesBottom = 0; - int newPagesTop = widget.pages.length - 1; - int oldEntriesTop = _history.length - 1; - - final List<_RouteEntry> newHistory = <_RouteEntry>[]; - final Map<_RouteEntry, List<_RouteEntry>> pageRouteToPagelessRoutes = <_RouteEntry, List<_RouteEntry>>{}; - - // Updates the bottom of the list. - _RouteEntry previousOldPageRouteEntry; - while (oldEntriesBottom <= oldEntriesTop) { - final _RouteEntry oldEntry = _history[oldEntriesBottom]; - assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed); - // Records pageless route. The bottom most pageless routes will be - // stored in key = null. - if (!oldEntry.hasPage) { - final List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes.putIfAbsent( - previousOldPageRouteEntry, - () => <_RouteEntry>[], - ); - pagelessRoutes.add(oldEntry); - oldEntriesBottom += 1; - continue; - } - if (newPagesBottom > newPagesTop) - break; - final Page newPage = widget.pages[newPagesBottom]; - if (!oldEntry.canUpdateFrom(newPage)) - break; - previousOldPageRouteEntry = oldEntry; - oldEntry.route._updateSettings(newPage); - newHistory.add(oldEntry); - newPagesBottom += 1; - oldEntriesBottom += 1; - } - - int pagelessRoutesToSkip = 0; - // Scans the top of the list until we found a page-based route that cannot be - // updated. - while ((oldEntriesBottom <= oldEntriesTop) && (newPagesBottom <= newPagesTop)) { - final _RouteEntry oldEntry = _history[oldEntriesTop]; - assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed); - if (!oldEntry.hasPage) { - // This route might need to be skipped if we can not find a page above. - pagelessRoutesToSkip += 1; - oldEntriesTop -= 1; - continue; - } - final Page newPage = widget.pages[newPagesTop]; - if (!oldEntry.canUpdateFrom(newPage)) - break; - // We found the page for all the consecutive pageless routes below. Those - // pageless routes do not need to be skipped. - pagelessRoutesToSkip = 0; - oldEntriesTop -= 1; - newPagesTop -= 1; - } - // Reverts the pageless routes that cannot be updated. - oldEntriesTop += pagelessRoutesToSkip; - - // Scans middle of the old entries and records the page key to old entry map. - int oldEntriesBottomToScan = oldEntriesBottom; - final Map pageKeyToOldEntry = {}; - while (oldEntriesBottomToScan <= oldEntriesTop) { - final _RouteEntry oldEntry = _history[oldEntriesBottomToScan]; - oldEntriesBottomToScan += 1; - assert( - oldEntry != null && - oldEntry.currentState != _RouteLifecycle.disposed - ); - // Pageless routes will be recorded when we update the middle of the old - // list. - if (!oldEntry.hasPage) - continue; - - assert(oldEntry.hasPage); - - final Page page = oldEntry.route.settings as Page; - if (page.key == null) - continue; - - assert(!pageKeyToOldEntry.containsKey(page.key)); - pageKeyToOldEntry[page.key] = oldEntry; - } - - // Updates the middle of the list. - while (newPagesBottom <= newPagesTop) { - final Page nextPage = widget.pages[newPagesBottom]; - newPagesBottom += 1; - if ( - nextPage.key == null || - !pageKeyToOldEntry.containsKey(nextPage.key) || - !pageKeyToOldEntry[nextPage.key].canUpdateFrom(nextPage) - ) { - // There is no matching key in the old history, we need to create a new - // route and wait for the transition delegate to decide how to add - // it into the history. - final _RouteEntry newEntry = _RouteEntry( - nextPage.createRoute(context), - initialState: _RouteLifecycle.staging, - ); - needsExplicitDecision = true; - assert( - newEntry.route.settings == nextPage, - 'If a route is created from a page, its must have that page as its ' - 'settings.', - ); - newHistory.add(newEntry); - } else { - // Removes the key from pageKeyToOldEntry to indicate it is taken. - final _RouteEntry matchingEntry = pageKeyToOldEntry.remove(nextPage.key); - assert(matchingEntry.canUpdateFrom(nextPage)); - matchingEntry.route._updateSettings(nextPage); - newHistory.add(matchingEntry); - } - } - - // Any remaining old routes that do not have a match will need to be removed. - final Map locationToExitingPageRoute = {}; - while (oldEntriesBottom <= oldEntriesTop) { - final _RouteEntry potentialEntryToRemove = _history[oldEntriesBottom]; - oldEntriesBottom += 1; - - if (!potentialEntryToRemove.hasPage) { - assert(previousOldPageRouteEntry != null); - final List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes - .putIfAbsent( - previousOldPageRouteEntry, - () => <_RouteEntry>[] - ); - pagelessRoutes.add(potentialEntryToRemove); - assert(() { - potentialEntryToRemove._debugWaitingForExitDecision = previousOldPageRouteEntry._debugWaitingForExitDecision; - return true; - }()); - continue; - } - - final Page potentialPageToRemove = potentialEntryToRemove.route.settings as Page; - // Marks for transition delegate to remove if this old page does not have - // a key or was not taken during updating the middle of new page. - if ( - potentialPageToRemove.key == null || - pageKeyToOldEntry.containsKey(potentialPageToRemove.key) - ) { - locationToExitingPageRoute[previousOldPageRouteEntry] = potentialEntryToRemove; - assert(() { - potentialEntryToRemove._debugWaitingForExitDecision = true; - return true; - }()); - } - previousOldPageRouteEntry = potentialEntryToRemove; - } - - // We've scanned the whole list. - assert(oldEntriesBottom == oldEntriesTop + 1); - assert(newPagesBottom == newPagesTop + 1); - newPagesTop = widget.pages.length - 1; - oldEntriesTop = _history.length - 1; - // Verifies we either reach the bottom or the oldEntriesBottom must be updatable - // by newPagesBottom. - assert(() { - if (oldEntriesBottom <= oldEntriesTop) - return newPagesBottom <= newPagesTop && - _history[oldEntriesBottom].hasPage && - _history[oldEntriesBottom].canUpdateFrom(widget.pages[newPagesBottom]); - else - return newPagesBottom > newPagesTop; - }()); - - // Updates the top of the list. - while ((oldEntriesBottom <= oldEntriesTop) && (newPagesBottom <= newPagesTop)) { - final _RouteEntry oldEntry = _history[oldEntriesBottom]; - assert(oldEntry != null && oldEntry.currentState != _RouteLifecycle.disposed); - if (!oldEntry.hasPage) { - assert(previousOldPageRouteEntry != null); - final List<_RouteEntry> pagelessRoutes = pageRouteToPagelessRoutes - .putIfAbsent( - previousOldPageRouteEntry, - () => <_RouteEntry>[] - ); - pagelessRoutes.add(oldEntry); - continue; - } - previousOldPageRouteEntry = oldEntry; - final Page newPage = widget.pages[newPagesBottom]; - assert(oldEntry.canUpdateFrom(newPage)); - oldEntry.route._updateSettings(newPage); - newHistory.add(oldEntry); - oldEntriesBottom += 1; - newPagesBottom += 1; - } - - // Finally, uses transition delegate to make explicit decision if needed. - needsExplicitDecision = needsExplicitDecision || locationToExitingPageRoute.isNotEmpty; - Iterable<_RouteEntry> results = newHistory; - if (needsExplicitDecision) { - results = widget.transitionDelegate._transition( - newPageRouteHistory: newHistory, - locationToExitingPageRoute: locationToExitingPageRoute, - pageRouteToPagelessRoutes: pageRouteToPagelessRoutes, - ).cast<_RouteEntry>(); - } - _history = <_RouteEntry>[]; - // Adds the leading pageless routes if there is any. - if (pageRouteToPagelessRoutes.containsKey(null)) { - _history.addAll(pageRouteToPagelessRoutes[null]); - } - for (final _RouteEntry result in results) { - _history.add(result); - if (pageRouteToPagelessRoutes.containsKey(result)) { - _history.addAll(pageRouteToPagelessRoutes[result]); - } - } - assert(() {_debugUpdatingPage = false; return true;}()); - assert(() { _debugLocked = true; return true; }()); - _flushHistoryUpdates(); - assert(() { _debugLocked = false; return true; }()); - } - void _flushHistoryUpdates({bool rearrangeOverlay = true}) { - assert(_debugLocked && !_debugUpdatingPage); + assert(_debugLocked); // 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 @@ -2941,7 +1987,7 @@ class NavigatorState extends State with TickerProviderStateMixin { _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. + bool canRemove = false; Route 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>[]; @@ -2951,21 +1997,12 @@ class NavigatorState extends State with TickerProviderStateMixin { assert(rearrangeOverlay); entry.handleAdd( navigator: this, + previous: previous?.route, + previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route, + isNewFirst: next == null, ); - assert(entry.currentState == _RouteLifecycle.adding); + assert(entry.currentState == _RouteLifecycle.idle); continue; - case _RouteLifecycle.adding: - if (canRemoveOrAdd || next == null) { - entry.didAdd( - navigator: this, - previous: previous?.route, - previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route, - isNewFirst: next == null - ); - assert(entry.currentState == _RouteLifecycle.idle); - continue; - } - break; case _RouteLifecycle.push: case _RouteLifecycle.pushReplace: case _RouteLifecycle.replace: @@ -2994,7 +2031,7 @@ class NavigatorState extends State with TickerProviderStateMixin { seenTopActiveRoute = true; // This route is idle, so we are allowed to remove subsequent (earlier) // routes that are waiting to be removed silently: - canRemoveOrAdd = true; + canRemove = true; break; case _RouteLifecycle.pop: if (!seenTopActiveRoute) { @@ -3007,7 +2044,6 @@ class NavigatorState extends State with TickerProviderStateMixin { previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route, ); assert(entry.currentState == _RouteLifecycle.popping); - canRemoveOrAdd = true; break; case _RouteLifecycle.popping: // Will exit this state when animation completes. @@ -3025,7 +2061,7 @@ class NavigatorState extends State with TickerProviderStateMixin { assert(entry.currentState == _RouteLifecycle.removing); continue; case _RouteLifecycle.removing: - if (!canRemoveOrAdd && next != null) { + if (!canRemove && next != null) { // We aren't allowed to remove this route yet. break; } @@ -3037,7 +2073,6 @@ class NavigatorState extends State with TickerProviderStateMixin { entry = next; break; case _RouteLifecycle.disposed: - case _RouteLifecycle.staging: assert(false); break; } @@ -3073,11 +2108,7 @@ class NavigatorState extends State with TickerProviderStateMixin { int index = _history.length - 1; while (index >= 0) { final _RouteEntry entry = _history[index]; - if (!entry.suitableForAnnouncement) { - index -= 1; - continue; - } - final _RouteEntry next = _getRouteAfter(index + 1, _RouteEntry.suitableForTransitionAnimationPredicate); + final _RouteEntry next = _getRouteAfter(index + 1, _RouteEntry.isPresentPredicate); if (next?.route != entry.lastAnnouncedNextRoute) { if (entry.shouldAnnounceChangeToNext(next?.route)) { @@ -3085,7 +2116,7 @@ class NavigatorState extends State with TickerProviderStateMixin { } entry.lastAnnouncedNextRoute = next?.route; } - final _RouteEntry previous = _getRouteBefore(index - 1, _RouteEntry.suitableForTransitionAnimationPredicate); + final _RouteEntry previous = _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate); if (previous?.route != entry.lastAnnouncedPreviousRoute) { entry.route.didChangePrevious(previous?.route); entry.lastAnnouncedPreviousRoute = previous?.route; @@ -3584,12 +2615,7 @@ class NavigatorState extends State with TickerProviderStateMixin { return true; }()); final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate); - if (entry.hasPage) { - if (widget.onPopPage(entry.route, result)) - entry.currentState = _RouteLifecycle.pop; - } else { - entry.pop(result); - } + entry.pop(result); if (entry.currentState == _RouteLifecycle.pop) { // Flush the history if the route actually wants to be popped (the pop // wasn't handled internally). diff --git a/packages/flutter/test/widgets/navigator_test.dart b/packages/flutter/test/widgets/navigator_test.dart index 51855e6ef1..cc01fdb8db 100644 --- a/packages/flutter/test/widgets/navigator_test.dart +++ b/packages/flutter/test/widgets/navigator_test.dart @@ -1708,666 +1708,6 @@ void main() { await tester.pump(const Duration(seconds: 1)); expect(tickCount, 4); }); - - group('Page api', (){ - Widget buildNavigator( - List> pages, - PopPageCallback onPopPage, [ - GlobalKey key, - TransitionDelegate transitionDelegate - ]) { - return MediaQuery( - data: MediaQueryData.fromWindow(WidgetsBinding.instance.window), - child: Localizations( - locale: const Locale('en', 'US'), - delegates: const >[ - DefaultMaterialLocalizations.delegate, - DefaultWidgetsLocalizations.delegate - ], - child: Directionality( - textDirection: TextDirection.ltr, - child: Navigator( - key: key, - pages: pages, - onPopPage: onPopPage, - transitionDelegate: transitionDelegate ?? const DefaultTransitionDelegate(), - ), - ), - ), - ); - } - - testWidgets('can initialize with pages list', (WidgetTester tester) async { - final GlobalKey navigator = GlobalKey(); - final List myPages = [ - const TestPage(key: ValueKey('1'), name:'initial'), - const TestPage(key: ValueKey('2'), name:'second'), - const TestPage(key: ValueKey('3'), name:'third'), - ]; - - bool onPopPage(Route route, dynamic result) { - myPages.removeWhere((Page page) => route.settings == page); - return route.didPop(result); - } - - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - expect(find.text('third'), findsOneWidget); - expect(find.text('second'), findsNothing); - expect(find.text('initial'), findsNothing); - - navigator.currentState.pop(); - await tester.pumpAndSettle(); - expect(find.text('third'), findsNothing); - expect(find.text('second'), findsOneWidget); - expect(find.text('initial'), findsNothing); - - navigator.currentState.pop(); - await tester.pumpAndSettle(); - expect(find.text('third'), findsNothing); - expect(find.text('second'), findsNothing); - expect(find.text('initial'), findsOneWidget); - }); - - testWidgets('can push and pop pages using page api', (WidgetTester tester) async { - Animation secondaryAnimationOfRouteOne; - Animation primaryAnimationOfRouteOne; - Animation secondaryAnimationOfRouteTwo; - Animation primaryAnimationOfRouteTwo; - Animation secondaryAnimationOfRouteThree; - Animation primaryAnimationOfRouteThree; - final GlobalKey navigator = GlobalKey(); - List> myPages = >[ - CustomBuilderPage( - key: const ValueKey('1'), - name:'initial', - routeBuilder: (BuildContext context, RouteSettings settings) { - return PageRouteBuilder( - settings: settings, - pageBuilder: (_, Animation animation, Animation secondaryAnimation) { - secondaryAnimationOfRouteOne = secondaryAnimation; - primaryAnimationOfRouteOne = animation; - return const Text('initial'); - }, - ); - }, - ), - ]; - - bool onPopPage(Route route, dynamic result) { - myPages.removeWhere((Page page) => route.settings == page); - return route.didPop(result); - } - - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - expect(find.text('initial'), findsOneWidget); - - myPages = >[ - CustomBuilderPage( - key: const ValueKey('1'), - name:'initial', - routeBuilder: (BuildContext context, RouteSettings settings) { - return PageRouteBuilder( - settings: settings, - pageBuilder: (_, Animation animation, Animation secondaryAnimation) { - secondaryAnimationOfRouteOne = secondaryAnimation; - primaryAnimationOfRouteOne = animation; - return const Text('initial'); - }, - ); - }, - ), - CustomBuilderPage( - key: const ValueKey('2'), - name:'second', - routeBuilder: (BuildContext context, RouteSettings settings) { - return PageRouteBuilder( - settings: settings, - pageBuilder: (_, Animation animation, Animation secondaryAnimation) { - secondaryAnimationOfRouteTwo = secondaryAnimation; - primaryAnimationOfRouteTwo = animation; - return const Text('second'); - }, - ); - }, - ), - CustomBuilderPage( - key: const ValueKey('3'), - name:'third', - routeBuilder: (BuildContext context, RouteSettings settings) { - return PageRouteBuilder( - settings: settings, - pageBuilder: (_, Animation animation, Animation secondaryAnimation) { - secondaryAnimationOfRouteThree = secondaryAnimation; - primaryAnimationOfRouteThree = animation; - return const Text('third'); - }, - ); - }, - ) - ]; - - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - // The third page is transitioning, and the secondary animation of first - // page should chain with the third page. The animation of second page - // won't start until the third page finishes transition. - expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteThree.value); - expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteTwo.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteTwo.status, AnimationStatus.dismissed); - expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteThree.status, AnimationStatus.forward); - - await tester.pump(const Duration(milliseconds: 30)); - expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteThree.value); - expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteTwo.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteTwo.status, AnimationStatus.dismissed); - expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteThree.value, 0.1); - await tester.pumpAndSettle(); - // After transition finishes, the routes' animations are correctly chained. - expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value); - expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteThree.value); - expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed); - expect(find.text('third'), findsOneWidget); - expect(find.text('second'), findsNothing); - expect(find.text('initial'), findsNothing); - // Starts pops the pages using page api and verify the animations chain - // correctly. - - myPages = >[ - CustomBuilderPage( - key: const ValueKey('1'), - name:'initial', - routeBuilder: (BuildContext context, RouteSettings settings) { - return PageRouteBuilder( - settings: settings, - pageBuilder: (_, Animation animation, Animation secondaryAnimation) { - secondaryAnimationOfRouteOne = secondaryAnimation; - primaryAnimationOfRouteOne = animation; - return const Text('initial'); - }, - ); - }, - ), - CustomBuilderPage( - key: const ValueKey('2'), - name:'second', - routeBuilder: (BuildContext context, RouteSettings settings) { - return PageRouteBuilder( - settings: settings, - pageBuilder: (_, Animation animation, Animation secondaryAnimation) { - secondaryAnimationOfRouteTwo = secondaryAnimation; - primaryAnimationOfRouteTwo = animation; - return const Text('second'); - }, - ); - }, - ), - ]; - - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - await tester.pump(const Duration(milliseconds: 30)); - expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value); - expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteThree.value); - expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteThree.value, 0.9); - await tester.pumpAndSettle(); - expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value); - expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteThree.value); - expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteThree.status, AnimationStatus.dismissed); - }); - - testWidgets('can modify routes history and secondary animation still works', (WidgetTester tester) async { - final GlobalKey navigator = GlobalKey(); - Animation secondaryAnimationOfRouteOne; - Animation primaryAnimationOfRouteOne; - Animation secondaryAnimationOfRouteTwo; - Animation primaryAnimationOfRouteTwo; - Animation secondaryAnimationOfRouteThree; - Animation primaryAnimationOfRouteThree; - List> myPages = >[ - CustomBuilderPage( - key: const ValueKey('1'), - name:'initial', - routeBuilder: (BuildContext context, RouteSettings settings) { - return PageRouteBuilder( - settings: settings, - pageBuilder: (_, Animation animation, Animation secondaryAnimation) { - secondaryAnimationOfRouteOne = secondaryAnimation; - primaryAnimationOfRouteOne = animation; - return const Text('initial'); - }, - ); - }, - ), - CustomBuilderPage( - key: const ValueKey('2'), - name:'second', - routeBuilder: (BuildContext context, RouteSettings settings) { - return PageRouteBuilder( - settings: settings, - pageBuilder: (_, Animation animation, Animation secondaryAnimation) { - secondaryAnimationOfRouteTwo = secondaryAnimation; - primaryAnimationOfRouteTwo = animation; - return const Text('second'); - }, - ); - }, - ), - CustomBuilderPage( - key: const ValueKey('3'), - name:'third', - routeBuilder: (BuildContext context, RouteSettings settings) { - return PageRouteBuilder( - settings: settings, - pageBuilder: (_, Animation animation, Animation secondaryAnimation) { - secondaryAnimationOfRouteThree = secondaryAnimation; - primaryAnimationOfRouteThree = animation; - return const Text('third'); - }, - ); - }, - ), - ]; - bool onPopPage(Route route, dynamic result) { - myPages.removeWhere((Page page) => route.settings == page); - return route.didPop(result); - } - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - expect(find.text('third'), findsOneWidget); - expect(find.text('second'), findsNothing); - expect(find.text('initial'), findsNothing); - expect(secondaryAnimationOfRouteOne.value, primaryAnimationOfRouteTwo.value); - expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteThree.value); - expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteThree.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed); - - myPages = myPages.reversed.toList(); - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - // Reversed routes are still chained up correctly. - expect(secondaryAnimationOfRouteThree.value, primaryAnimationOfRouteTwo.value); - expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteOne.value); - expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteOne.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteOne.status, AnimationStatus.completed); - - navigator.currentState.pop(); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 30)); - expect(secondaryAnimationOfRouteThree.value, primaryAnimationOfRouteTwo.value); - expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteOne.value); - expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteOne.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteOne.value, 0.9); - await tester.pumpAndSettle(); - expect(secondaryAnimationOfRouteThree.value, primaryAnimationOfRouteTwo.value); - expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteOne.value); - expect(primaryAnimationOfRouteTwo.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteOne.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteOne.status, AnimationStatus.dismissed); - - navigator.currentState.pop(); - await tester.pump(); - await tester.pump(const Duration(milliseconds: 30)); - expect(secondaryAnimationOfRouteThree.value, primaryAnimationOfRouteTwo.value); - expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteOne.value); - expect(primaryAnimationOfRouteTwo.value, 0.9); - expect(secondaryAnimationOfRouteOne.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteOne.status, AnimationStatus.dismissed); - await tester.pumpAndSettle(); - expect(secondaryAnimationOfRouteThree.value, primaryAnimationOfRouteTwo.value); - expect(primaryAnimationOfRouteThree.status, AnimationStatus.completed); - expect(secondaryAnimationOfRouteTwo.value, primaryAnimationOfRouteOne.value); - expect(primaryAnimationOfRouteTwo.status, AnimationStatus.dismissed); - expect(secondaryAnimationOfRouteOne.status, AnimationStatus.dismissed); - expect(primaryAnimationOfRouteOne.status, AnimationStatus.dismissed); - }); - - testWidgets('can work with pageless route', (WidgetTester tester) async { - final GlobalKey navigator = GlobalKey(); - List myPages = [ - const TestPage(key: ValueKey('1'), name:'initial'), - const TestPage(key: ValueKey('2'), name:'second'), - ]; - - bool onPopPage(Route route, dynamic result) { - myPages.removeWhere((Page page) => route.settings == page); - return route.didPop(result); - } - - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - expect(find.text('second'), findsOneWidget); - expect(find.text('initial'), findsNothing); - // Pushes two pageless routes to second page route - navigator.currentState.push( - MaterialPageRoute( - builder: (BuildContext context) => const Text('second-pageless1'), - settings: null, - ) - ); - navigator.currentState.push( - MaterialPageRoute( - builder: (BuildContext context) => const Text('second-pageless2'), - settings: null, - ) - ); - await tester.pumpAndSettle(); - // Now the history should look like - // [initial, second, second-pageless1, second-pageless2]. - expect(find.text('initial'), findsNothing); - expect(find.text('second'), findsNothing); - expect(find.text('second-pageless1'), findsNothing); - expect(find.text('second-pageless2'), findsOneWidget); - - myPages = [ - const TestPage(key: ValueKey('1'), name:'initial'), - const TestPage(key: ValueKey('2'), name:'second'), - const TestPage(key: ValueKey('3'), name:'third'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - await tester.pumpAndSettle(); - expect(find.text('initial'), findsNothing); - expect(find.text('second'), findsNothing); - expect(find.text('second-pageless1'), findsNothing); - expect(find.text('second-pageless2'), findsNothing); - expect(find.text('third'), findsOneWidget); - - // Pushes one pageless routes to third page route - navigator.currentState.push( - MaterialPageRoute( - builder: (BuildContext context) => const Text('third-pageless1'), - settings: null, - ) - ); - await tester.pumpAndSettle(); - // Now the history should look like - // [initial, second, second-pageless1, second-pageless2, third, third-pageless1]. - expect(find.text('initial'), findsNothing); - expect(find.text('second'), findsNothing); - expect(find.text('second-pageless1'), findsNothing); - expect(find.text('second-pageless2'), findsNothing); - expect(find.text('third'), findsNothing); - expect(find.text('third-pageless1'), findsOneWidget); - - myPages = [ - const TestPage(key: ValueKey('1'), name:'initial'), - const TestPage(key: ValueKey('3'), name:'third'), - const TestPage(key: ValueKey('2'), name:'second'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - // Swaps the order without any adding or removing should not trigger any - // transition. The routes should update without a pumpAndSettle - // Now the history should look like - // [initial, third, third-pageless1, second, second-pageless1, second-pageless2]. - expect(find.text('initial'), findsNothing); - expect(find.text('third'), findsNothing); - expect(find.text('third-pageless1'), findsNothing); - expect(find.text('second'), findsNothing); - expect(find.text('second-pageless1'), findsNothing); - expect(find.text('second-pageless2'), findsOneWidget); - // Pops the route one by one to make sure the order is correct. - navigator.currentState.pop(); - await tester.pumpAndSettle(); - expect(find.text('initial'), findsNothing); - expect(find.text('third'), findsNothing); - expect(find.text('third-pageless1'), findsNothing); - expect(find.text('second'), findsNothing); - expect(find.text('second-pageless1'), findsOneWidget); - expect(find.text('second-pageless2'), findsNothing); - expect(myPages.length, 3); - navigator.currentState.pop(); - await tester.pumpAndSettle(); - expect(find.text('initial'), findsNothing); - expect(find.text('third'), findsNothing); - expect(find.text('third-pageless1'), findsNothing); - expect(find.text('second'), findsOneWidget); - expect(find.text('second-pageless1'), findsNothing); - expect(find.text('second-pageless2'), findsNothing); - expect(myPages.length, 3); - navigator.currentState.pop(); - await tester.pumpAndSettle(); - expect(find.text('initial'), findsNothing); - expect(find.text('third'), findsNothing); - expect(find.text('third-pageless1'), findsOneWidget); - expect(find.text('second'), findsNothing); - expect(find.text('second-pageless1'), findsNothing); - expect(find.text('second-pageless2'), findsNothing); - expect(myPages.length, 2); - navigator.currentState.pop(); - await tester.pumpAndSettle(); - expect(find.text('initial'), findsNothing); - expect(find.text('third'), findsOneWidget); - expect(find.text('third-pageless1'), findsNothing); - expect(find.text('second'), findsNothing); - expect(find.text('second-pageless1'), findsNothing); - expect(find.text('second-pageless2'), findsNothing); - expect(myPages.length, 2); - navigator.currentState.pop(); - await tester.pumpAndSettle(); - expect(find.text('initial'), findsOneWidget); - expect(find.text('third'), findsNothing); - expect(find.text('third-pageless1'), findsNothing); - expect(find.text('second'), findsNothing); - expect(find.text('second-pageless1'), findsNothing); - expect(find.text('second-pageless2'), findsNothing); - expect(myPages.length, 1); - }); - - testWidgets('complex case 1', (WidgetTester tester) async { - final GlobalKey navigator = GlobalKey(); - List myPages = [ - const TestPage(key: ValueKey('1'), name: 'initial'), - ]; - bool onPopPage(Route route, dynamic result) { - myPages.removeWhere((Page page) => route.settings == page); - return route.didPop(result); - } - - // Add initial page route with one pageless route. - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - bool initialPageless1Completed = false; - navigator.currentState.push( - MaterialPageRoute( - builder: (BuildContext context) => const Text('initial-pageless1'), - settings: null, - ) - ).then((_) => initialPageless1Completed = true); - await tester.pumpAndSettle(); - - // Pushes second page route with two pageless routes. - myPages = [ - const TestPage(key: ValueKey('1'), name: 'initial'), - const TestPage(key: ValueKey('2'), name: 'second'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - await tester.pumpAndSettle(); - bool secondPageless1Completed = false; - navigator.currentState.push( - MaterialPageRoute( - builder: (BuildContext context) => const Text('second-pageless1'), - settings: null, - ) - ).then((_) => secondPageless1Completed = true); - await tester.pumpAndSettle(); - bool secondPageless2Completed = false; - navigator.currentState.push( - MaterialPageRoute( - builder: (BuildContext context) => const Text('second-pageless2'), - settings: null, - ) - ).then((_) => secondPageless2Completed = true); - await tester.pumpAndSettle(); - - // Pushes third page route with one pageless route. - myPages = [ - const TestPage(key: ValueKey('1'), name: 'initial'), - const TestPage(key: ValueKey('2'), name: 'second'), - const TestPage(key: ValueKey('3'), name: 'third'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - await tester.pumpAndSettle(); - bool thirdPageless1Completed = false; - navigator.currentState.push( - MaterialPageRoute( - builder: (BuildContext context) => const Text('third-pageless1'), - settings: null, - ) - ).then((_) => thirdPageless1Completed = true); - await tester.pumpAndSettle(); - - // Nothing has been popped. - expect(initialPageless1Completed, false); - expect(secondPageless1Completed, false); - expect(secondPageless2Completed, false); - expect(thirdPageless1Completed, false); - - // Switches order and removes the initial page route. - myPages = [ - const TestPage(key: ValueKey('3'), name: 'third'), - const TestPage(key: ValueKey('2'), name: 'second'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - // The pageless route of initial page route should be completed. - expect(initialPageless1Completed, true); - expect(secondPageless1Completed, false); - expect(secondPageless2Completed, false); - expect(thirdPageless1Completed, false); - - myPages = [ - const TestPage(key: ValueKey('3'), name: 'third'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - await tester.pumpAndSettle(); - expect(secondPageless1Completed, true); - expect(secondPageless2Completed, true); - expect(thirdPageless1Completed, false); - - myPages = [ - const TestPage(key: ValueKey('4'), name: 'forth'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator)); - expect(thirdPageless1Completed, true); - await tester.pumpAndSettle(); - expect(find.text('forth'), findsOneWidget); - }); - - testWidgets('complex case 1 - with always remove transition delegate', (WidgetTester tester) async { - final GlobalKey navigator = GlobalKey(); - final AlwaysRemoveTransitionDelegate transitionDelegate = AlwaysRemoveTransitionDelegate(); - List myPages = [ - const TestPage(key: ValueKey('1'), name: 'initial'), - ]; - bool onPopPage(Route route, dynamic result) { - myPages.removeWhere((Page page) => route.settings == page); - return route.didPop(result); - } - - // Add initial page route with one pageless route. - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator, transitionDelegate)); - bool initialPageless1Completed = false; - navigator.currentState.push( - MaterialPageRoute( - builder: (BuildContext context) => const Text('initial-pageless1'), - settings: null, - ) - ).then((_) => initialPageless1Completed = true); - await tester.pumpAndSettle(); - - // Pushes second page route with two pageless routes. - myPages = [ - const TestPage(key: ValueKey('1'), name: 'initial'), - const TestPage(key: ValueKey('2'), name: 'second'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator, transitionDelegate)); - bool secondPageless1Completed = false; - navigator.currentState.push( - MaterialPageRoute( - builder: (BuildContext context) => const Text('second-pageless1'), - settings: null, - ) - ).then((_) => secondPageless1Completed = true); - await tester.pumpAndSettle(); - bool secondPageless2Completed = false; - navigator.currentState.push( - MaterialPageRoute( - builder: (BuildContext context) => const Text('second-pageless2'), - settings: null, - ) - ).then((_) => secondPageless2Completed = true); - await tester.pumpAndSettle(); - - // Pushes third page route with one pageless route. - myPages = [ - const TestPage(key: ValueKey('1'), name: 'initial'), - const TestPage(key: ValueKey('2'), name: 'second'), - const TestPage(key: ValueKey('3'), name: 'third'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator, transitionDelegate)); - bool thirdPageless1Completed = false; - navigator.currentState.push( - MaterialPageRoute( - builder: (BuildContext context) => const Text('third-pageless1'), - settings: null, - ) - ).then((_) => thirdPageless1Completed = true); - await tester.pumpAndSettle(); - - // Nothing has been popped. - expect(initialPageless1Completed, false); - expect(secondPageless1Completed, false); - expect(secondPageless2Completed, false); - expect(thirdPageless1Completed, false); - - // Switches order and removes the initial page route. - myPages = [ - const TestPage(key: ValueKey('3'), name: 'third'), - const TestPage(key: ValueKey('2'), name: 'second'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator, transitionDelegate)); - // The pageless route of initial page route should be removed without complete. - expect(initialPageless1Completed, false); - expect(secondPageless1Completed, false); - expect(secondPageless2Completed, false); - expect(thirdPageless1Completed, false); - - myPages = [ - const TestPage(key: ValueKey('3'), name: 'third'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator, transitionDelegate)); - await tester.pumpAndSettle(); - expect(initialPageless1Completed, false); - expect(secondPageless1Completed, false); - expect(secondPageless2Completed, false); - expect(thirdPageless1Completed, false); - - myPages = [ - const TestPage(key: ValueKey('4'), name: 'forth'), - ]; - await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator, transitionDelegate)); - await tester.pump(); - expect(initialPageless1Completed, false); - expect(secondPageless1Completed, false); - expect(secondPageless2Completed, false); - expect(thirdPageless1Completed, false); - expect(find.text('forth'), findsOneWidget); - }); - - }); } class _TickingWidget extends StatefulWidget { @@ -2402,62 +1742,6 @@ class _TickingWidgetState extends State<_TickingWidget> with SingleTickerProvide } } -class AlwaysRemoveTransitionDelegate extends TransitionDelegate { - @override - Iterable resolve({ - List newPageRouteHistory, - Map locationToExitingPageRoute, - Map> pageRouteToPagelessRoutes, - }) { - final List results = []; - void handleExitingRoute(RouteTransitionRecord location) { - if (!locationToExitingPageRoute.containsKey(location)) - return; - - final RouteTransitionRecord exitingPageRoute = locationToExitingPageRoute[location]; - final bool hasPagelessRoute = pageRouteToPagelessRoutes.containsKey(exitingPageRoute); - - exitingPageRoute.markForRemove(); - results.add(exitingPageRoute); - - if (hasPagelessRoute) { - final List pagelessRoutes = pageRouteToPagelessRoutes[exitingPageRoute]; - for (final RouteTransitionRecord pagelessRoute in pagelessRoutes) { - pagelessRoute.markForRemove(); - } - } - handleExitingRoute(exitingPageRoute); - } - handleExitingRoute(null); - - for (final RouteTransitionRecord pageRoute in newPageRouteHistory) { - if (pageRoute.isEntering) { - pageRoute.markForAdd(); - } - results.add(pageRoute); - handleExitingRoute(pageRoute); - - } - return results; - } -} - -class TestPage extends Page { - const TestPage({ - LocalKey key, - String name, - Object arguments, - }) : super(key: key, name: name, arguments: arguments); - - @override - Route createRoute(BuildContext context) { - return MaterialPageRoute( - builder: (BuildContext context) => Text(name), - settings: this, - ); - } -} - class NoAnimationPageRoute extends PageRouteBuilder { NoAnimationPageRoute({WidgetBuilder pageBuilder}) : super(pageBuilder: (BuildContext context, __, ___) {