Unverified Commit aee9e94c authored by chunhtai's avatar chunhtai Committed by GitHub

Revert "Implements the navigator page api (#50362)" (#53610)

This reverts commit 9a6eb7de.
parent 0d4b4fb5
...@@ -36,11 +36,6 @@ import 'ticker_provider.dart'; ...@@ -36,11 +36,6 @@ import 'ticker_provider.dart';
/// * [Navigator], which is where all the [Route]s end up. /// * [Navigator], which is where all the [Route]s end up.
typedef RouteFactory = Route<dynamic> Function(RouteSettings settings); 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. /// Creates a series of one or more routes.
/// ///
/// Used by [Navigator.onGenerateInitialRoutes]. /// Used by [Navigator.onGenerateInitialRoutes].
...@@ -55,15 +50,6 @@ typedef RoutePredicate = bool Function(Route<dynamic> route); ...@@ -55,15 +50,6 @@ typedef RoutePredicate = bool Function(Route<dynamic> route);
/// [ModalRoute.removeScopedWillPopCallback], and [WillPopScope]. /// [ModalRoute.removeScopedWillPopCallback], and [WillPopScope].
typedef WillPopCallback = Future<bool> Function(); 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. /// Indicates whether the current route should be popped.
/// ///
/// Used as the return value for [Route.willPop]. /// Used as the return value for [Route.willPop].
...@@ -104,13 +90,6 @@ enum RoutePopDisposition { ...@@ -104,13 +90,6 @@ enum RoutePopDisposition {
/// See [MaterialPageRoute] for a route that replaces the entire screen with a /// See [MaterialPageRoute] for a route that replaces the entire screen with a
/// platform-adaptive transition. /// 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 /// 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 /// [currentResult], [popped], and [didPop]. The type `void` may be used if the
/// route does not return a value. /// route does not return a value.
...@@ -119,7 +98,7 @@ abstract class Route<T> { ...@@ -119,7 +98,7 @@ abstract class Route<T> {
/// ///
/// If the [settings] are not provided, an empty [RouteSettings] object is /// If the [settings] are not provided, an empty [RouteSettings] object is
/// used instead. /// 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. /// The navigator that the route is in, if any.
NavigatorState get navigator => _navigator; NavigatorState get navigator => _navigator;
...@@ -128,26 +107,7 @@ abstract class Route<T> { ...@@ -128,26 +107,7 @@ abstract class Route<T> {
/// The settings for this route. /// The settings for this route.
/// ///
/// See [RouteSettings] for details. /// See [RouteSettings] for details.
/// final RouteSettings settings;
/// 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();
}
}
/// The overlay entries of this route. /// The overlay entries of this route.
/// ///
...@@ -287,6 +247,7 @@ abstract class Route<T> { ...@@ -287,6 +247,7 @@ abstract class Route<T> {
/// ///
/// See [popped], [didComplete], and [currentResult] for a discussion of the /// See [popped], [didComplete], and [currentResult] for a discussion of the
/// `result` argument. /// `result` argument.
@protected
@mustCallSuper @mustCallSuper
bool didPop(T result) { bool didPop(T result) {
didComplete(result); didComplete(result);
...@@ -486,82 +447,6 @@ class RouteSettings { ...@@ -486,82 +447,6 @@ class RouteSettings {
String toString() => '${objectRuntimeType(this, 'RouteSettings')}("$name", $arguments)'; 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]. /// An interface for observing the behavior of a [Navigator].
class NavigatorObserver { class NavigatorObserver {
/// The navigator that the observer is observing, if any. /// The navigator that the observer is observing, if any.
...@@ -606,336 +491,6 @@ class NavigatorObserver { ...@@ -606,336 +491,6 @@ class NavigatorObserver {
void didStopUserGesture() { } 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. /// 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 /// Many apps have a navigator near the top of their widget hierarchy in order
...@@ -950,9 +505,8 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> { ...@@ -950,9 +505,8 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> {
/// Mobile apps typically reveal their contents via full-screen elements /// Mobile apps typically reveal their contents via full-screen elements
/// called "screens" or "pages". In Flutter these elements are called /// called "screens" or "pages". In Flutter these elements are called
/// routes and they're managed by a [Navigator] widget. The navigator /// routes and they're managed by a [Navigator] widget. The navigator
/// manages a stack of [Route] objects and provides two ways for managing /// manages a stack of [Route] objects and provides methods for managing
/// the stack, the declarative API [Navigator.pages] or imperative API /// the stack, like [Navigator.push] and [Navigator.pop].
/// [Navigator.push] and [Navigator.pop].
/// ///
/// When your user interface fits this paradigm of a stack, where the user /// 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, /// should be able to _navigate_ back to an earlier element in the stack,
...@@ -964,21 +518,6 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> { ...@@ -964,21 +518,6 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> {
/// used in the [Scaffold.appBar] property) can automatically add a back /// used in the [Scaffold.appBar] property) can automatically add a back
/// button for user navigation. /// 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 /// ### Displaying a full-screen route
/// ///
/// Although you can create a navigator directly, it's most common to use the /// Although you can create a navigator directly, it's most common to use the
...@@ -1194,7 +733,7 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> { ...@@ -1194,7 +733,7 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> {
/// ``` /// ```
/// ///
/// ```dart main /// ```dart main
/// void main() => runApp(MyApp()); /// void main() => runApp(new MyApp());
/// ``` /// ```
/// ///
/// ```dart /// ```dart
...@@ -1320,77 +859,17 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> { ...@@ -1320,77 +859,17 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> {
class Navigator extends StatefulWidget { class Navigator extends StatefulWidget {
/// Creates a widget that maintains a stack-based history of child widgets. /// Creates a widget that maintains a stack-based history of child widgets.
/// ///
/// The [onGenerateRoute], [pages], [onGenerateInitialRoutes], /// The [onGenerateRoute] argument must not be null.
/// [transitionDelegate], [observers] arguments must not be null.
///
/// If the [pages] is not empty, the [onPopPage] must not be null.
const Navigator({ const Navigator({
Key key, Key key,
this.pages = const <Page<dynamic>>[],
this.onPopPage,
this.initialRoute, this.initialRoute,
this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes, this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes,
this.onGenerateRoute, this.onGenerateRoute,
this.onUnknownRoute, this.onUnknownRoute,
this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(),
this.observers = const <NavigatorObserver>[], this.observers = const <NavigatorObserver>[],
}) : assert(pages != null), }) : assert(onGenerateInitialRoutes != null),
assert(onGenerateInitialRoutes != null),
assert(transitionDelegate != null),
assert(observers != null),
super(key: key); 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. /// The name of the first route to show.
/// ///
/// Defaults to [Navigator.defaultRouteName]. /// Defaults to [Navigator.defaultRouteName].
...@@ -2201,14 +1680,6 @@ class Navigator extends StatefulWidget { ...@@ -2201,14 +1680,6 @@ class Navigator extends StatefulWidget {
// The _RouteLifecycle state machine (only goes down): // The _RouteLifecycle state machine (only goes down):
// //
// [creation of a _RouteEntry] // [creation of a _RouteEntry]
// |
// +
// |\
// | \
// | staging
// | /
// |/
// +-+----------+--+-------+
// / | | | // / | | |
// / | | | // / | | |
// / | | | // / | | |
...@@ -2217,7 +1688,7 @@ class Navigator extends StatefulWidget { ...@@ -2217,7 +1688,7 @@ class Navigator extends StatefulWidget {
// pushReplace push* add* replace* // pushReplace push* add* replace*
// \ | | | // \ | | |
// \ | | / // \ | | /
// +--pushing# adding / // +--pushing# | /
// \ / / // \ / /
// \ / / // \ / /
// idle--+-----+ // idle--+-----+
...@@ -2243,69 +1714,47 @@ class Navigator extends StatefulWidget { ...@@ -2243,69 +1714,47 @@ class Navigator extends StatefulWidget {
// route entry will exit that state. // route entry will exit that state.
// # These states await futures or other events, then transition automatically. // # These states await futures or other events, then transition automatically.
enum _RouteLifecycle { enum _RouteLifecycle {
staging, // we will wait for transition delegate to decide what to do with this route. // routes that are and will be present:
//
// routes that are present:
//
add, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages 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 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 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 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 replace, // we'll want to run install, didReplace, etc; a route added via replace() and friends
idle, // route is being harmless idle, // route is being harmless
// // routes that are but will not present:
// routes that are not present:
//
// routes that should be included in route announcement and should still listen to transition changes.
pop, // we'll want to call didPop pop, // we'll want to call didPop
remove, // we'll want to run didReplace/didRemove etc 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 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 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 dispose, // we will dispose the route momentarily
disposed, // we have disposed the route disposed, // we have disposed the route
} }
typedef _RouteEntryPredicate = bool Function(_RouteEntry entry); typedef _RouteEntryPredicate = bool Function(_RouteEntry entry);
class _RouteEntry extends RouteTransitionRecord { class _RouteEntry {
_RouteEntry( _RouteEntry(
this.route, { this.route, {
@required _RouteLifecycle initialState, @required _RouteLifecycle initialState,
}) : assert(route != null), }) : assert(route != null),
assert(initialState != null), assert(initialState != null),
assert( assert(
initialState == _RouteLifecycle.staging ||
initialState == _RouteLifecycle.add || initialState == _RouteLifecycle.add ||
initialState == _RouteLifecycle.push || initialState == _RouteLifecycle.push ||
initialState == _RouteLifecycle.pushReplace || initialState == _RouteLifecycle.pushReplace ||
initialState == _RouteLifecycle.replace initialState == _RouteLifecycle.replace
), ),
currentState = initialState; currentState = initialState; // ignore: prefer_initializing_formals
@override
final Route<dynamic> route; final Route<dynamic> route;
_RouteLifecycle currentState; _RouteLifecycle currentState;
Route<dynamic> lastAnnouncedNextRoute; // last argument to Route.didChangeNext
Route<dynamic> lastAnnouncedPreviousRoute; // last argument to Route.didChangePrevious Route<dynamic> lastAnnouncedPreviousRoute; // last argument to Route.didChangePrevious
Route<dynamic> lastAnnouncedPoppedNextRoute; // last argument to Route.didPopNext 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) { void handleAdd({ @required NavigatorState navigator, @required bool isNewFirst, @required Route<dynamic> previous, @required Route<dynamic> previousPresent }) {
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}) {
assert(currentState == _RouteLifecycle.add); assert(currentState == _RouteLifecycle.add);
assert(navigator != null); assert(navigator != null);
assert(navigator._debugLocked); assert(navigator._debugLocked);
...@@ -2313,7 +1762,13 @@ class _RouteEntry extends RouteTransitionRecord { ...@@ -2313,7 +1762,13 @@ class _RouteEntry extends RouteTransitionRecord {
route._navigator = navigator; route._navigator = navigator;
route.install(); route.install();
assert(route.overlayEntries.isNotEmpty); 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 }) { void handlePush({ @required NavigatorState navigator, @required bool isNewFirst, @required Route<dynamic> previous, @required Route<dynamic> previousPresent }) {
...@@ -2383,16 +1838,6 @@ class _RouteEntry extends RouteTransitionRecord { ...@@ -2383,16 +1838,6 @@ class _RouteEntry extends RouteTransitionRecord {
bool doingPop = false; 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) { void pop<T>(T result) {
assert(isPresent); assert(isPresent);
doingPop = true; doingPop = true;
...@@ -2406,11 +1851,6 @@ class _RouteEntry extends RouteTransitionRecord { ...@@ -2406,11 +1851,6 @@ class _RouteEntry extends RouteTransitionRecord {
// Route is removed without being completed. // Route is removed without being completed.
void remove({ bool isReplaced = false }) { 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) if (currentState.index >= _RouteLifecycle.remove.index)
return; return;
assert(isPresent); assert(isPresent);
...@@ -2420,11 +1860,6 @@ class _RouteEntry extends RouteTransitionRecord { ...@@ -2420,11 +1860,6 @@ class _RouteEntry extends RouteTransitionRecord {
// Route completes with `result` and is removed. // Route completes with `result` and is removed.
void complete<T>(T result, { bool isReplaced = false }) { 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) if (currentState.index >= _RouteLifecycle.remove.index)
return; return;
assert(isPresent); assert(isPresent);
...@@ -2445,25 +1880,8 @@ class _RouteEntry extends RouteTransitionRecord { ...@@ -2445,25 +1880,8 @@ class _RouteEntry extends RouteTransitionRecord {
currentState = _RouteLifecycle.disposed; currentState = _RouteLifecycle.disposed;
} }
bool get willBePresent { bool get willBePresent => currentState.index <= _RouteLifecycle.idle.index;
return currentState.index <= _RouteLifecycle.idle.index && bool get isPresent => currentState.index <= _RouteLifecycle.remove.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 shouldAnnounceChangeToNext(Route<dynamic> nextRoute) { bool shouldAnnounceChangeToNext(Route<dynamic> nextRoute) {
assert(nextRoute != lastAnnouncedNextRoute); assert(nextRoute != lastAnnouncedNextRoute);
...@@ -2477,76 +1895,17 @@ class _RouteEntry extends RouteTransitionRecord { ...@@ -2477,76 +1895,17 @@ class _RouteEntry extends RouteTransitionRecord {
} }
static final _RouteEntryPredicate isPresentPredicate = (_RouteEntry entry) => entry.isPresent; 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 final _RouteEntryPredicate willBePresentPredicate = (_RouteEntry entry) => entry.willBePresent;
static _RouteEntryPredicate isRoutePredicate(Route<dynamic> route) { static _RouteEntryPredicate isRoutePredicate(Route<dynamic> route) {
return (_RouteEntry entry) => entry.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<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. /// The state for a [Navigator] widget.
class NavigatorState extends State<Navigator> with TickerProviderStateMixin { class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final GlobalKey<OverlayState> _overlayKey = GlobalKey<OverlayState>(); final GlobalKey<OverlayState> _overlayKey = GlobalKey<OverlayState>();
List<_RouteEntry> _history = <_RouteEntry>[]; final List<_RouteEntry> _history = <_RouteEntry>[];
/// The [FocusScopeNode] for the [FocusScope] that encloses the routes. /// The [FocusScopeNode] for the [FocusScope] that encloses the routes.
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope'); final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope');
...@@ -2556,40 +1915,20 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2556,40 +1915,20 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
@override @override
void initState() { void initState() {
super.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) { for (final NavigatorObserver observer in widget.observers) {
assert(observer.navigator == null); assert(observer.navigator == null);
observer._navigator = this; observer._navigator = this;
} }
String initialRoute = widget.initialRoute; // TODO(chunhtai): Uses pages after we add page api.
if (widget.pages.isNotEmpty) { // https://github.com/flutter/flutter/issues/45938
_history.addAll( _history.addAll(
widget.pages.map((Page<dynamic> page) => _RouteEntry( widget.onGenerateInitialRoutes(this, widget.initialRoute ?? Navigator.defaultRouteName)
page.createRoute(context), .map((Route<dynamic> route) => _RouteEntry(
route,
initialState: _RouteLifecycle.add, 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);
assert(() { _debugLocked = true; return true; }()); assert(() { _debugLocked = true; return true; }());
_flushHistoryUpdates(); _flushHistoryUpdates();
...@@ -2599,10 +1938,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2599,10 +1938,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
@override @override
void didUpdateWidget(Navigator oldWidget) { void didUpdateWidget(Navigator oldWidget) {
super.didUpdateWidget(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) { if (oldWidget.observers != widget.observers) {
for (final NavigatorObserver observer in oldWidget.observers) for (final NavigatorObserver observer in oldWidget.observers)
observer._navigator = null; observer._navigator = null;
...@@ -2611,31 +1946,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2611,31 +1946,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
observer._navigator = this; 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) for (final _RouteEntry entry in _history)
entry.route.changedExternalState(); 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 @override
void dispose() { void dispose() {
assert(!_debugLocked); assert(!_debugLocked);
...@@ -2663,276 +1977,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2663,276 +1977,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
String _lastAnnouncedRouteName; 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}) { void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
assert(_debugLocked && !_debugUpdatingPage); assert(_debugLocked);
// Clean up the list, sending updates to the routes that changed. Notably, // Clean up the list, sending updates to the routes that changed. Notably,
// we don't send the didChangePrevious/didChangeNext updates to those that // 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 // did not change at this point, because we're not yet sure exactly what the
...@@ -2941,7 +1987,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2941,7 +1987,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_RouteEntry next; _RouteEntry next;
_RouteEntry entry = _history[index]; _RouteEntry entry = _history[index];
_RouteEntry previous = index > 0 ? _history[index - 1] : null; _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. 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. bool seenTopActiveRoute = false; // Whether we've seen the route that would get didPopNext.
final List<_RouteEntry> toBeDisposed = <_RouteEntry>[]; final List<_RouteEntry> toBeDisposed = <_RouteEntry>[];
...@@ -2951,21 +1997,12 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2951,21 +1997,12 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
assert(rearrangeOverlay); assert(rearrangeOverlay);
entry.handleAdd( entry.handleAdd(
navigator: this, 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; 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.push:
case _RouteLifecycle.pushReplace: case _RouteLifecycle.pushReplace:
case _RouteLifecycle.replace: case _RouteLifecycle.replace:
...@@ -2994,7 +2031,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2994,7 +2031,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
seenTopActiveRoute = true; seenTopActiveRoute = true;
// This route is idle, so we are allowed to remove subsequent (earlier) // This route is idle, so we are allowed to remove subsequent (earlier)
// routes that are waiting to be removed silently: // routes that are waiting to be removed silently:
canRemoveOrAdd = true; canRemove = true;
break; break;
case _RouteLifecycle.pop: case _RouteLifecycle.pop:
if (!seenTopActiveRoute) { if (!seenTopActiveRoute) {
...@@ -3007,7 +2044,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -3007,7 +2044,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route, previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
); );
assert(entry.currentState == _RouteLifecycle.popping); assert(entry.currentState == _RouteLifecycle.popping);
canRemoveOrAdd = true;
break; break;
case _RouteLifecycle.popping: case _RouteLifecycle.popping:
// Will exit this state when animation completes. // Will exit this state when animation completes.
...@@ -3025,7 +2061,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -3025,7 +2061,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
assert(entry.currentState == _RouteLifecycle.removing); assert(entry.currentState == _RouteLifecycle.removing);
continue; continue;
case _RouteLifecycle.removing: case _RouteLifecycle.removing:
if (!canRemoveOrAdd && next != null) { if (!canRemove && next != null) {
// We aren't allowed to remove this route yet. // We aren't allowed to remove this route yet.
break; break;
} }
...@@ -3037,7 +2073,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -3037,7 +2073,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
entry = next; entry = next;
break; break;
case _RouteLifecycle.disposed: case _RouteLifecycle.disposed:
case _RouteLifecycle.staging:
assert(false); assert(false);
break; break;
} }
...@@ -3073,11 +2108,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -3073,11 +2108,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
int index = _history.length - 1; int index = _history.length - 1;
while (index >= 0) { while (index >= 0) {
final _RouteEntry entry = _history[index]; final _RouteEntry entry = _history[index];
if (!entry.suitableForAnnouncement) { final _RouteEntry next = _getRouteAfter(index + 1, _RouteEntry.isPresentPredicate);
index -= 1;
continue;
}
final _RouteEntry next = _getRouteAfter(index + 1, _RouteEntry.suitableForTransitionAnimationPredicate);
if (next?.route != entry.lastAnnouncedNextRoute) { if (next?.route != entry.lastAnnouncedNextRoute) {
if (entry.shouldAnnounceChangeToNext(next?.route)) { if (entry.shouldAnnounceChangeToNext(next?.route)) {
...@@ -3085,7 +2116,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -3085,7 +2116,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
} }
entry.lastAnnouncedNextRoute = next?.route; 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) { if (previous?.route != entry.lastAnnouncedPreviousRoute) {
entry.route.didChangePrevious(previous?.route); entry.route.didChangePrevious(previous?.route);
entry.lastAnnouncedPreviousRoute = previous?.route; entry.lastAnnouncedPreviousRoute = previous?.route;
...@@ -3584,12 +2615,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -3584,12 +2615,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return true; return true;
}()); }());
final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate); final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate);
if (entry.hasPage) { entry.pop<T>(result);
if (widget.onPopPage(entry.route, result))
entry.currentState = _RouteLifecycle.pop;
} else {
entry.pop<T>(result);
}
if (entry.currentState == _RouteLifecycle.pop) { if (entry.currentState == _RouteLifecycle.pop) {
// Flush the history if the route actually wants to be popped (the pop // Flush the history if the route actually wants to be popped (the pop
// wasn't handled internally). // wasn't handled internally).
......
...@@ -1708,666 +1708,6 @@ void main() { ...@@ -1708,666 +1708,6 @@ void main() {
await tester.pump(const Duration(seconds: 1)); await tester.pump(const Duration(seconds: 1));
expect(tickCount, 4); 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 { class _TickingWidget extends StatefulWidget {
...@@ -2402,62 +1742,6 @@ class _TickingWidgetState extends State<_TickingWidget> with SingleTickerProvide ...@@ -2402,62 +1742,6 @@ class _TickingWidgetState extends State<_TickingWidget> with SingleTickerProvide
} }
} }
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> { class NoAnimationPageRoute extends PageRouteBuilder<void> {
NoAnimationPageRoute({WidgetBuilder pageBuilder}) NoAnimationPageRoute({WidgetBuilder pageBuilder})
: super(pageBuilder: (BuildContext context, __, ___) { : super(pageBuilder: (BuildContext context, __, ___) {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment