Revert "Implements the navigator page api (#50362)" (#53610)
This reverts commit 9a6eb7def3e932bc54f554200ec1fe7cdf2e630e.
diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart
index b57d352..c81cae0 100644
--- a/packages/flutter/lib/src/widgets/navigator.dart
+++ b/packages/flutter/lib/src/widgets/navigator.dart
@@ -36,11 +36,6 @@
/// * [Navigator], which is where all the [Route]s end up.
typedef RouteFactory = Route<dynamic> Function(RouteSettings settings);
-/// Creates a route for the given context and route settings.
-///
-/// Used by [CustomBuilderPage.routeBuilder].
-typedef RouteBuilder<T> = Route<T> Function(BuildContext context, RouteSettings settings);
-
/// Creates a series of one or more routes.
///
/// Used by [Navigator.onGenerateInitialRoutes].
@@ -55,15 +50,6 @@
/// [ModalRoute.removeScopedWillPopCallback], and [WillPopScope].
typedef WillPopCallback = Future<bool> 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<dynamic> route, dynamic result);
-
/// Indicates whether the current route should be popped.
///
/// Used as the return value for [Route.willPop].
@@ -104,13 +90,6 @@
/// 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 @@
///
/// 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 @@
/// 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 @@
///
/// See [popped], [didComplete], and [currentResult] for a discussion of the
/// `result` argument.
+ @protected
@mustCallSuper
bool didPop(T result) {
didComplete(result);
@@ -486,82 +447,6 @@
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<T> 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<dynamic> 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<T> 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<T> extends Page<T> {
- /// 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<T> routeBuilder;
-
- @override
- Route<T> createRoute(BuildContext context) {
- final Route<T> 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 @@
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<dynamic> 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<void> {
-/// @override
-/// Iterable<RouteTransitionRecord> resolve({
-/// List<RouteTransitionRecord> newPageRouteHistory,
-/// Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
-/// Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
-/// }) {
-/// final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
-///
-/// 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<RouteTransitionRecord> 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<T> {
- /// Creates a delegate and enables subclass to create a constant class.
- const TransitionDelegate();
-
- Iterable<RouteTransitionRecord> _transition({
- List<RouteTransitionRecord> newPageRouteHistory,
- Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
- Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
- }) {
- final Iterable<RouteTransitionRecord> 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<RouteTransitionRecord> resultsToVerify = results.toList(growable: false);
- final Set<RouteTransitionRecord> 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<RouteTransitionRecord> resolve({
- List<RouteTransitionRecord> newPageRouteHistory,
- Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
- Map<RouteTransitionRecord, List<RouteTransitionRecord>> 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<T> extends TransitionDelegate<T> {
- /// Creates a default transition delegate.
- const DefaultTransitionDelegate() : super();
-
- @override
- Iterable<RouteTransitionRecord> resolve({
- List<RouteTransitionRecord> newPageRouteHistory,
- Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
- Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
- }) {
- final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
- // 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<RouteTransitionRecord> 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 @@
/// 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 @@
/// 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 @@
/// ```
///
/// ```dart main
-/// void main() => runApp(MyApp());
+/// void main() => runApp(new MyApp());
/// ```
///
/// ```dart
@@ -1320,77 +859,17 @@
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 <Page<dynamic>>[],
- this.onPopPage,
this.initialRoute,
this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes,
this.onGenerateRoute,
this.onUnknownRoute,
- this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(),
this.observers = const <NavigatorObserver>[],
- }) : 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<Page<dynamic>> 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<dynamic> transitionDelegate;
-
/// The name of the first route to show.
///
/// Defaults to [Navigator.defaultRouteName].
@@ -2201,14 +1680,6 @@
// The _RouteLifecycle state machine (only goes down):
//
// [creation of a _RouteEntry]
-// |
-// +
-// |\
-// | \
-// | staging
-// | /
-// |/
-// +-+----------+--+-------+
// / | | |
// / | | |
// / | | |
@@ -2217,7 +1688,7 @@
// pushReplace push* add* replace*
// \ | | |
// \ | | /
-// +--pushing# adding /
+// +--pushing# | /
// \ / /
// \ / /
// idle--+-----+
@@ -2243,69 +1714,47 @@
// 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<dynamic> route;
_RouteLifecycle currentState;
+ Route<dynamic> lastAnnouncedNextRoute; // last argument to Route.didChangeNext
Route<dynamic> lastAnnouncedPreviousRoute; // last argument to Route.didChangePrevious
Route<dynamic> lastAnnouncedPoppedNextRoute; // last argument to Route.didPopNext
- Route<dynamic> lastAnnouncedNextRoute; // last argument to Route.didChangeNext
- bool get hasPage => route.settings is Page;
-
- bool canUpdateFrom(Page<dynamic> page) {
- if (currentState.index > _RouteLifecycle.idle.index)
- return false;
- if (!hasPage)
- return false;
- final Page<dynamic> routePage = route.settings as Page<dynamic>;
- return page.canUpdate(routePage);
- }
-
- void handleAdd({ @required NavigatorState navigator}) {
+ void handleAdd({ @required NavigatorState navigator, @required bool isNewFirst, @required Route<dynamic> previous, @required Route<dynamic> previousPresent }) {
assert(currentState == _RouteLifecycle.add);
assert(navigator != null);
assert(navigator._debugLocked);
@@ -2313,7 +1762,13 @@
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<dynamic> previous, @required Route<dynamic> previousPresent }) {
@@ -2383,16 +1838,6 @@
bool doingPop = false;
- void didAdd({ @required NavigatorState navigator, @required bool isNewFirst, @required Route<dynamic> previous, @required Route<dynamic> 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>(T result) {
assert(isPresent);
doingPop = true;
@@ -2406,11 +1851,6 @@
// 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 @@
// Route completes with `result` and is removed.
void complete<T>(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 @@
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<dynamic> nextRoute) {
assert(nextRoute != lastAnnouncedNextRoute);
@@ -2477,76 +1895,17 @@
}
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<dynamic> 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<dynamic>(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<dynamic>(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<Navigator> with TickerProviderStateMixin {
final GlobalKey<OverlayState> _overlayKey = GlobalKey<OverlayState>();
- 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 @@
@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<dynamic> 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<dynamic> 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<dynamic> route) =>
- _RouteEntry(
- route,
- initialState: _RouteLifecycle.add,
- ),
),
- );
- }
+ ),
+ );
assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }());
_flushHistoryUpdates();
@@ -2599,10 +1938,6 @@
@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 @@
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<Key> keyReservation = <Key>{};
- for (final Page<dynamic> 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 @@
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<dynamic> 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<dynamic> 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<LocalKey, _RouteEntry> pageKeyToOldEntry = <LocalKey, _RouteEntry>{};
- 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<dynamic> page = oldEntry.route.settings as Page<dynamic>;
- 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<dynamic> 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<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute = <RouteTransitionRecord, RouteTransitionRecord>{};
- 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<dynamic> potentialPageToRemove = potentialEntryToRemove.route.settings as Page<dynamic>;
- // 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<dynamic> 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 @@
_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<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>[];
@@ -2951,21 +1997,12 @@
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 @@
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 @@
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 @@
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 @@
entry = next;
break;
case _RouteLifecycle.disposed:
- case _RouteLifecycle.staging:
assert(false);
break;
}
@@ -3073,11 +2108,7 @@
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 @@
}
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 @@
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<T>(result);
- }
+ entry.pop<T>(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 51855e6..cc01fdb 100644
--- a/packages/flutter/test/widgets/navigator_test.dart
+++ b/packages/flutter/test/widgets/navigator_test.dart
@@ -1708,666 +1708,6 @@
await tester.pump(const Duration(seconds: 1));
expect(tickCount, 4);
});
-
- group('Page api', (){
- Widget buildNavigator(
- List<Page<dynamic>> pages,
- PopPageCallback onPopPage, [
- GlobalKey<NavigatorState> key,
- TransitionDelegate<dynamic> transitionDelegate
- ]) {
- return MediaQuery(
- data: MediaQueryData.fromWindow(WidgetsBinding.instance.window),
- child: Localizations(
- locale: const Locale('en', 'US'),
- delegates: const <LocalizationsDelegate<dynamic>>[
- DefaultMaterialLocalizations.delegate,
- DefaultWidgetsLocalizations.delegate
- ],
- child: Directionality(
- textDirection: TextDirection.ltr,
- child: Navigator(
- key: key,
- pages: pages,
- onPopPage: onPopPage,
- transitionDelegate: transitionDelegate ?? const DefaultTransitionDelegate<dynamic>(),
- ),
- ),
- ),
- );
- }
-
- testWidgets('can initialize with pages list', (WidgetTester tester) async {
- final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
- final List<TestPage> myPages = <TestPage>[
- const TestPage(key: ValueKey<String>('1'), name:'initial'),
- const TestPage(key: ValueKey<String>('2'), name:'second'),
- const TestPage(key: ValueKey<String>('3'), name:'third'),
- ];
-
- bool onPopPage(Route<dynamic> route, dynamic result) {
- myPages.removeWhere((Page<dynamic> 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<double> secondaryAnimationOfRouteOne;
- Animation<double> primaryAnimationOfRouteOne;
- Animation<double> secondaryAnimationOfRouteTwo;
- Animation<double> primaryAnimationOfRouteTwo;
- Animation<double> secondaryAnimationOfRouteThree;
- Animation<double> primaryAnimationOfRouteThree;
- final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
- List<Page<dynamic>> myPages = <Page<dynamic>>[
- CustomBuilderPage<void>(
- key: const ValueKey<String>('1'),
- name:'initial',
- routeBuilder: (BuildContext context, RouteSettings settings) {
- return PageRouteBuilder<void>(
- settings: settings,
- pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
- secondaryAnimationOfRouteOne = secondaryAnimation;
- primaryAnimationOfRouteOne = animation;
- return const Text('initial');
- },
- );
- },
- ),
- ];
-
- bool onPopPage(Route<dynamic> route, dynamic result) {
- myPages.removeWhere((Page<dynamic> page) => route.settings == page);
- return route.didPop(result);
- }
-
- await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator));
- expect(find.text('initial'), findsOneWidget);
-
- myPages = <Page<dynamic>>[
- CustomBuilderPage<void>(
- key: const ValueKey<String>('1'),
- name:'initial',
- routeBuilder: (BuildContext context, RouteSettings settings) {
- return PageRouteBuilder<void>(
- settings: settings,
- pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
- secondaryAnimationOfRouteOne = secondaryAnimation;
- primaryAnimationOfRouteOne = animation;
- return const Text('initial');
- },
- );
- },
- ),
- CustomBuilderPage<void>(
- key: const ValueKey<String>('2'),
- name:'second',
- routeBuilder: (BuildContext context, RouteSettings settings) {
- return PageRouteBuilder<void>(
- settings: settings,
- pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
- secondaryAnimationOfRouteTwo = secondaryAnimation;
- primaryAnimationOfRouteTwo = animation;
- return const Text('second');
- },
- );
- },
- ),
- CustomBuilderPage<void>(
- key: const ValueKey<String>('3'),
- name:'third',
- routeBuilder: (BuildContext context, RouteSettings settings) {
- return PageRouteBuilder<void>(
- settings: settings,
- pageBuilder: (_, Animation<double> animation, Animation<double> 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 = <Page<dynamic>>[
- CustomBuilderPage<void>(
- key: const ValueKey<String>('1'),
- name:'initial',
- routeBuilder: (BuildContext context, RouteSettings settings) {
- return PageRouteBuilder<void>(
- settings: settings,
- pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
- secondaryAnimationOfRouteOne = secondaryAnimation;
- primaryAnimationOfRouteOne = animation;
- return const Text('initial');
- },
- );
- },
- ),
- CustomBuilderPage<void>(
- key: const ValueKey<String>('2'),
- name:'second',
- routeBuilder: (BuildContext context, RouteSettings settings) {
- return PageRouteBuilder<void>(
- settings: settings,
- pageBuilder: (_, Animation<double> animation, Animation<double> 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<NavigatorState> navigator = GlobalKey<NavigatorState>();
- Animation<double> secondaryAnimationOfRouteOne;
- Animation<double> primaryAnimationOfRouteOne;
- Animation<double> secondaryAnimationOfRouteTwo;
- Animation<double> primaryAnimationOfRouteTwo;
- Animation<double> secondaryAnimationOfRouteThree;
- Animation<double> primaryAnimationOfRouteThree;
- List<Page<dynamic>> myPages = <CustomBuilderPage<void>>[
- CustomBuilderPage<void>(
- key: const ValueKey<String>('1'),
- name:'initial',
- routeBuilder: (BuildContext context, RouteSettings settings) {
- return PageRouteBuilder<void>(
- settings: settings,
- pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
- secondaryAnimationOfRouteOne = secondaryAnimation;
- primaryAnimationOfRouteOne = animation;
- return const Text('initial');
- },
- );
- },
- ),
- CustomBuilderPage<void>(
- key: const ValueKey<String>('2'),
- name:'second',
- routeBuilder: (BuildContext context, RouteSettings settings) {
- return PageRouteBuilder<void>(
- settings: settings,
- pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
- secondaryAnimationOfRouteTwo = secondaryAnimation;
- primaryAnimationOfRouteTwo = animation;
- return const Text('second');
- },
- );
- },
- ),
- CustomBuilderPage<void>(
- key: const ValueKey<String>('3'),
- name:'third',
- routeBuilder: (BuildContext context, RouteSettings settings) {
- return PageRouteBuilder<void>(
- settings: settings,
- pageBuilder: (_, Animation<double> animation, Animation<double> secondaryAnimation) {
- secondaryAnimationOfRouteThree = secondaryAnimation;
- primaryAnimationOfRouteThree = animation;
- return const Text('third');
- },
- );
- },
- ),
- ];
- bool onPopPage(Route<dynamic> route, dynamic result) {
- myPages.removeWhere((Page<dynamic> 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<NavigatorState> navigator = GlobalKey<NavigatorState>();
- List<TestPage> myPages = <TestPage>[
- const TestPage(key: ValueKey<String>('1'), name:'initial'),
- const TestPage(key: ValueKey<String>('2'), name:'second'),
- ];
-
- bool onPopPage(Route<dynamic> route, dynamic result) {
- myPages.removeWhere((Page<dynamic> 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<void>(
- builder: (BuildContext context) => const Text('second-pageless1'),
- settings: null,
- )
- );
- navigator.currentState.push(
- MaterialPageRoute<void>(
- 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 = <TestPage>[
- const TestPage(key: ValueKey<String>('1'), name:'initial'),
- const TestPage(key: ValueKey<String>('2'), name:'second'),
- const TestPage(key: ValueKey<String>('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<void>(
- 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 = <TestPage>[
- const TestPage(key: ValueKey<String>('1'), name:'initial'),
- const TestPage(key: ValueKey<String>('3'), name:'third'),
- const TestPage(key: ValueKey<String>('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<NavigatorState> navigator = GlobalKey<NavigatorState>();
- List<TestPage> myPages = <TestPage>[
- const TestPage(key: ValueKey<String>('1'), name: 'initial'),
- ];
- bool onPopPage(Route<dynamic> route, dynamic result) {
- myPages.removeWhere((Page<dynamic> 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<void>(
- builder: (BuildContext context) => const Text('initial-pageless1'),
- settings: null,
- )
- ).then((_) => initialPageless1Completed = true);
- await tester.pumpAndSettle();
-
- // Pushes second page route with two pageless routes.
- myPages = <TestPage>[
- const TestPage(key: ValueKey<String>('1'), name: 'initial'),
- const TestPage(key: ValueKey<String>('2'), name: 'second'),
- ];
- await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator));
- await tester.pumpAndSettle();
- bool secondPageless1Completed = false;
- navigator.currentState.push(
- MaterialPageRoute<void>(
- builder: (BuildContext context) => const Text('second-pageless1'),
- settings: null,
- )
- ).then((_) => secondPageless1Completed = true);
- await tester.pumpAndSettle();
- bool secondPageless2Completed = false;
- navigator.currentState.push(
- MaterialPageRoute<void>(
- builder: (BuildContext context) => const Text('second-pageless2'),
- settings: null,
- )
- ).then((_) => secondPageless2Completed = true);
- await tester.pumpAndSettle();
-
- // Pushes third page route with one pageless route.
- myPages = <TestPage>[
- const TestPage(key: ValueKey<String>('1'), name: 'initial'),
- const TestPage(key: ValueKey<String>('2'), name: 'second'),
- const TestPage(key: ValueKey<String>('3'), name: 'third'),
- ];
- await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator));
- await tester.pumpAndSettle();
- bool thirdPageless1Completed = false;
- navigator.currentState.push(
- MaterialPageRoute<void>(
- 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 = <TestPage>[
- const TestPage(key: ValueKey<String>('3'), name: 'third'),
- const TestPage(key: ValueKey<String>('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 = <TestPage>[
- const TestPage(key: ValueKey<String>('3'), name: 'third'),
- ];
- await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator));
- await tester.pumpAndSettle();
- expect(secondPageless1Completed, true);
- expect(secondPageless2Completed, true);
- expect(thirdPageless1Completed, false);
-
- myPages = <TestPage>[
- const TestPage(key: ValueKey<String>('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<NavigatorState> navigator = GlobalKey<NavigatorState>();
- final AlwaysRemoveTransitionDelegate transitionDelegate = AlwaysRemoveTransitionDelegate();
- List<TestPage> myPages = <TestPage>[
- const TestPage(key: ValueKey<String>('1'), name: 'initial'),
- ];
- bool onPopPage(Route<dynamic> route, dynamic result) {
- myPages.removeWhere((Page<dynamic> 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<void>(
- builder: (BuildContext context) => const Text('initial-pageless1'),
- settings: null,
- )
- ).then((_) => initialPageless1Completed = true);
- await tester.pumpAndSettle();
-
- // Pushes second page route with two pageless routes.
- myPages = <TestPage>[
- const TestPage(key: ValueKey<String>('1'), name: 'initial'),
- const TestPage(key: ValueKey<String>('2'), name: 'second'),
- ];
- await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator, transitionDelegate));
- bool secondPageless1Completed = false;
- navigator.currentState.push(
- MaterialPageRoute<void>(
- builder: (BuildContext context) => const Text('second-pageless1'),
- settings: null,
- )
- ).then((_) => secondPageless1Completed = true);
- await tester.pumpAndSettle();
- bool secondPageless2Completed = false;
- navigator.currentState.push(
- MaterialPageRoute<void>(
- builder: (BuildContext context) => const Text('second-pageless2'),
- settings: null,
- )
- ).then((_) => secondPageless2Completed = true);
- await tester.pumpAndSettle();
-
- // Pushes third page route with one pageless route.
- myPages = <TestPage>[
- const TestPage(key: ValueKey<String>('1'), name: 'initial'),
- const TestPage(key: ValueKey<String>('2'), name: 'second'),
- const TestPage(key: ValueKey<String>('3'), name: 'third'),
- ];
- await tester.pumpWidget(buildNavigator(myPages, onPopPage, navigator, transitionDelegate));
- bool thirdPageless1Completed = false;
- navigator.currentState.push(
- MaterialPageRoute<void>(
- 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 = <TestPage>[
- const TestPage(key: ValueKey<String>('3'), name: 'third'),
- const TestPage(key: ValueKey<String>('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 = <TestPage>[
- const TestPage(key: ValueKey<String>('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 = <TestPage>[
- const TestPage(key: ValueKey<String>('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 AlwaysRemoveTransitionDelegate extends TransitionDelegate<void> {
- @override
- Iterable<RouteTransitionRecord> resolve({
- List<RouteTransitionRecord> newPageRouteHistory,
- Map<RouteTransitionRecord, RouteTransitionRecord> locationToExitingPageRoute,
- Map<RouteTransitionRecord, List<RouteTransitionRecord>> pageRouteToPagelessRoutes,
- }) {
- final List<RouteTransitionRecord> results = <RouteTransitionRecord>[];
- 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<RouteTransitionRecord> 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<void> {
- const TestPage({
- LocalKey key,
- String name,
- Object arguments,
- }) : super(key: key, name: name, arguments: arguments);
-
- @override
- Route<void> createRoute(BuildContext context) {
- return MaterialPageRoute<void>(
- builder: (BuildContext context) => Text(name),
- settings: this,
- );
- }
-}
-
class NoAnimationPageRoute extends PageRouteBuilder<void> {
NoAnimationPageRoute({WidgetBuilder pageBuilder})
: super(pageBuilder: (BuildContext context, __, ___) {