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

Navigator 2.0: Refactor the imperative api to continue working in the new...

Navigator 2.0: Refactor the imperative api to continue working in the new navigation system (#44930)
parent 3f2c6ea7
...@@ -103,7 +103,6 @@ void main() { ...@@ -103,7 +103,6 @@ void main() {
expect(route['settings'] is Map<dynamic, dynamic>); expect(route['settings'] is Map<dynamic, dynamic>);
final Map<dynamic, dynamic> settings = route['settings'] as Map<dynamic, dynamic>; final Map<dynamic, dynamic> settings = route['settings'] as Map<dynamic, dynamic>;
expect(settings.containsKey('name')); expect(settings.containsKey('name'));
expect(settings['isInitialRoute'] is bool);
run.stdin.write('q'); run.stdin.write('q');
final int result = await run.exitCode; final int result = await run.exitCode;
......
...@@ -61,7 +61,7 @@ class VideoCard extends StatelessWidget { ...@@ -61,7 +61,7 @@ class VideoCard extends StatelessWidget {
void pushFullScreenWidget() { void pushFullScreenWidget() {
final TransitionRoute<void> route = PageRouteBuilder<void>( final TransitionRoute<void> route = PageRouteBuilder<void>(
settings: RouteSettings(name: title, isInitialRoute: false), settings: RouteSettings(name: title),
pageBuilder: fullScreenRoutePageBuilder, pageBuilder: fullScreenRoutePageBuilder,
); );
......
...@@ -75,6 +75,7 @@ class CupertinoApp extends StatefulWidget { ...@@ -75,6 +75,7 @@ class CupertinoApp extends StatefulWidget {
this.routes = const <String, WidgetBuilder>{}, this.routes = const <String, WidgetBuilder>{},
this.initialRoute, this.initialRoute,
this.onGenerateRoute, this.onGenerateRoute,
this.onGenerateInitialRoutes,
this.onUnknownRoute, this.onUnknownRoute,
this.navigatorObservers = const <NavigatorObserver>[], this.navigatorObservers = const <NavigatorObserver>[],
this.builder, this.builder,
...@@ -131,6 +132,9 @@ class CupertinoApp extends StatefulWidget { ...@@ -131,6 +132,9 @@ class CupertinoApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.onGenerateRoute} /// {@macro flutter.widgets.widgetsApp.onGenerateRoute}
final RouteFactory onGenerateRoute; final RouteFactory onGenerateRoute;
/// {@macro flutter.widgets.widgetsApp.onGenerateInitialRoutes}
final InitialRouteListFactory onGenerateInitialRoutes;
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute} /// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
final RouteFactory onUnknownRoute; final RouteFactory onUnknownRoute;
...@@ -348,6 +352,7 @@ class _CupertinoAppState extends State<CupertinoApp> { ...@@ -348,6 +352,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
routes: widget.routes, routes: widget.routes,
initialRoute: widget.initialRoute, initialRoute: widget.initialRoute,
onGenerateRoute: widget.onGenerateRoute, onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute, onUnknownRoute: widget.onUnknownRoute,
builder: widget.builder, builder: widget.builder,
title: widget.title, title: widget.title,
......
...@@ -169,6 +169,7 @@ class MaterialApp extends StatefulWidget { ...@@ -169,6 +169,7 @@ class MaterialApp extends StatefulWidget {
this.routes = const <String, WidgetBuilder>{}, this.routes = const <String, WidgetBuilder>{},
this.initialRoute, this.initialRoute,
this.onGenerateRoute, this.onGenerateRoute,
this.onGenerateInitialRoutes,
this.onUnknownRoute, this.onUnknownRoute,
this.navigatorObservers = const <NavigatorObserver>[], this.navigatorObservers = const <NavigatorObserver>[],
this.builder, this.builder,
...@@ -224,6 +225,9 @@ class MaterialApp extends StatefulWidget { ...@@ -224,6 +225,9 @@ class MaterialApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.onGenerateRoute} /// {@macro flutter.widgets.widgetsApp.onGenerateRoute}
final RouteFactory onGenerateRoute; final RouteFactory onGenerateRoute;
/// {@macro flutter.widgets.widgetsApp.onGenerateInitialRoutes}
final InitialRouteListFactory onGenerateInitialRoutes;
/// {@macro flutter.widgets.widgetsApp.onUnknownRoute} /// {@macro flutter.widgets.widgetsApp.onUnknownRoute}
final RouteFactory onUnknownRoute; final RouteFactory onUnknownRoute;
...@@ -624,6 +628,7 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -624,6 +628,7 @@ class _MaterialAppState extends State<MaterialApp> {
routes: widget.routes, routes: widget.routes,
initialRoute: widget.initialRoute, initialRoute: widget.initialRoute,
onGenerateRoute: widget.onGenerateRoute, onGenerateRoute: widget.onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes,
onUnknownRoute: widget.onUnknownRoute, onUnknownRoute: widget.onUnknownRoute,
builder: (BuildContext context, Widget child) { builder: (BuildContext context, Widget child) {
// Use a light theme, dark theme, or fallback theme. // Use a light theme, dark theme, or fallback theme.
......
...@@ -91,6 +91,11 @@ typedef GenerateAppTitle = String Function(BuildContext context); ...@@ -91,6 +91,11 @@ typedef GenerateAppTitle = String Function(BuildContext context);
/// Creates a [PageRoute] using the given [RouteSettings] and [WidgetBuilder]. /// Creates a [PageRoute] using the given [RouteSettings] and [WidgetBuilder].
typedef PageRouteFactory = PageRoute<T> Function<T>(RouteSettings settings, WidgetBuilder builder); typedef PageRouteFactory = PageRoute<T> Function<T>(RouteSettings settings, WidgetBuilder builder);
/// The signature of [WidgetsApp.onGenerateInitialRoutes].
///
/// Creates a series of one or more initial routes.
typedef InitialRouteListFactory = List<Route<dynamic>> Function(String initialRoute);
/// A convenience widget that wraps a number of widgets that are commonly /// A convenience widget that wraps a number of widgets that are commonly
/// required for an application. /// required for an application.
/// ///
...@@ -164,6 +169,7 @@ class WidgetsApp extends StatefulWidget { ...@@ -164,6 +169,7 @@ class WidgetsApp extends StatefulWidget {
Key key, Key key,
this.navigatorKey, this.navigatorKey,
this.onGenerateRoute, this.onGenerateRoute,
this.onGenerateInitialRoutes,
this.onUnknownRoute, this.onUnknownRoute,
this.navigatorObservers = const <NavigatorObserver>[], this.navigatorObservers = const <NavigatorObserver>[],
this.initialRoute, this.initialRoute,
...@@ -191,6 +197,12 @@ class WidgetsApp extends StatefulWidget { ...@@ -191,6 +197,12 @@ class WidgetsApp extends StatefulWidget {
this.actions, this.actions,
}) : assert(navigatorObservers != null), }) : assert(navigatorObservers != null),
assert(routes != null), assert(routes != null),
assert(
home == null ||
onGenerateInitialRoutes == null,
'If onGenerateInitialRoutes is specifiied, the home argument will be '
'redundant.'
),
assert( assert(
home == null || home == null ||
!routes.containsKey(Navigator.defaultRouteName), !routes.containsKey(Navigator.defaultRouteName),
...@@ -290,6 +302,16 @@ class WidgetsApp extends StatefulWidget { ...@@ -290,6 +302,16 @@ class WidgetsApp extends StatefulWidget {
/// default handler will know what routes and [PageRoute]s to build. /// default handler will know what routes and [PageRoute]s to build.
final RouteFactory onGenerateRoute; final RouteFactory onGenerateRoute;
/// {@template flutter.widgets.widgetsApp.onGenerateInitialRoutes}
/// The routes generator callback used for generating initial routes if
/// [initialRoute] is provided.
///
/// If this property is not set, the underlying
/// [Navigator.onGenerateInitialRoutes] will default to
/// [Navigator.defaultGenerateInitialRoutes].
/// {@endtemplate}
final InitialRouteListFactory onGenerateInitialRoutes;
/// The [PageRoute] generator callback used when the app is navigated to a /// The [PageRoute] generator callback used when the app is navigated to a
/// named route. /// named route.
/// ///
...@@ -1265,6 +1287,11 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1265,6 +1287,11 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
? WidgetsBinding.instance.window.defaultRouteName ? WidgetsBinding.instance.window.defaultRouteName
: widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName, : widget.initialRoute ?? WidgetsBinding.instance.window.defaultRouteName,
onGenerateRoute: _onGenerateRoute, onGenerateRoute: _onGenerateRoute,
onGenerateInitialRoutes: widget.onGenerateInitialRoutes == null
? Navigator.defaultGenerateInitialRoutes
: (NavigatorState navigator, String initialRouteName) {
return widget.onGenerateInitialRoutes(initialRouteName);
},
onUnknownRoute: _onUnknownRoute, onUnknownRoute: _onUnknownRoute,
observers: widget.navigatorObservers, observers: widget.navigatorObservers,
); );
......
...@@ -29,9 +29,18 @@ import 'ticker_provider.dart'; ...@@ -29,9 +29,18 @@ import 'ticker_provider.dart';
/// Creates a route for the given route settings. /// Creates a route for the given route settings.
/// ///
/// Used by [Navigator.onGenerateRoute] and [Navigator.onUnknownRoute]. /// Used by [Navigator.onGenerateRoute].
///
/// See also:
///
/// * [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 series of one or more routes.
///
/// Used by [Navigator.onGenerateInitialRoutes].
typedef RouteListFactory = List<Route<dynamic>> Function(NavigatorState navigator, String initialRoute);
/// Signature for the [Navigator.popUntil] predicate argument. /// Signature for the [Navigator.popUntil] predicate argument.
typedef RoutePredicate = bool Function(Route<dynamic> route); typedef RoutePredicate = bool Function(Route<dynamic> route);
...@@ -87,11 +96,15 @@ const String _routeReplacedMethod = 'routeReplaced'; ...@@ -87,11 +96,15 @@ const String _routeReplacedMethod = 'routeReplaced';
/// visual affordances, which they place in the navigators [Overlay] using one /// visual affordances, which they place in the navigators [Overlay] using one
/// or more [OverlayEntry] objects. /// or more [OverlayEntry] objects.
/// ///
/// See [Navigator] for more explanation of how to use a Route /// See [Navigator] for more explanation of how to use a [Route] with
/// with navigation, including code examples. /// navigation, including code examples.
/// ///
/// See [MaterialPageRoute] for a route that replaces the /// See [MaterialPageRoute] for a route that replaces the entire screen with a
/// entire screen with a platform-adaptive transition. /// platform-adaptive transition.
///
/// The type argument `T` is the route's return type, as used by
/// [currentResult], [popped], and [didPop]. The type `void` may be used if the
/// route does not return a value.
abstract class Route<T> { abstract class Route<T> {
/// Initialize the [Route]. /// Initialize the [Route].
/// ///
...@@ -108,28 +121,35 @@ abstract class Route<T> { ...@@ -108,28 +121,35 @@ abstract class Route<T> {
/// See [RouteSettings] for details. /// See [RouteSettings] for details.
final RouteSettings settings; final RouteSettings settings;
/// The overlay entries for this route. /// The overlay entries of this route.
///
/// These are typically populated by [install]. The [Navigator] is in charge
/// of adding them to and removing them from the [Overlay].
///
/// There must be at least one entry in this list after [install] has been
/// invoked.
///
/// The [Navigator] will take care of keeping the entries together if the
/// route is moved in the history.
List<OverlayEntry> get overlayEntries => const <OverlayEntry>[]; List<OverlayEntry> get overlayEntries => const <OverlayEntry>[];
/// Called when the route is inserted into the navigator. /// Called when the route is inserted into the navigator.
/// ///
/// Use this to populate [overlayEntries] and add them to the overlay /// Uses this to populate [overlayEntries]. There must be at least one entry in
/// (accessible as [Navigator.overlay]). (The reason the [Route] is /// this list after [install] has been invoked. The [Navigator] will be in charge
/// responsible for doing this, rather than the [Navigator], is that the /// to add them to the [Overlay] or remove them from it by calling
/// [Route] will be responsible for _removing_ the entries and this way it's /// [OverlayEntry.remove].
/// symmetric.)
///
/// The `insertionPoint` argument will be null if this is the first route
/// inserted. Otherwise, it indicates the overlay entry to place immediately
/// below the first overlay for this route.
@protected @protected
@mustCallSuper @mustCallSuper
void install(OverlayEntry insertionPoint) { } void install() { }
/// Called after [install] when the route is pushed onto the navigator. /// Called after [install] when the route is pushed onto the navigator.
/// ///
/// The returned value resolves when the push transition is complete. /// The returned value resolves when the push transition is complete.
/// ///
/// The [didAdd] method will be called instead of [didPush] when the route
/// immediately appears on screen without any push transition.
///
/// The [didChangeNext] and [didChangePrevious] methods are typically called /// The [didChangeNext] and [didChangePrevious] methods are typically called
/// immediately after this method is called. /// immediately after this method is called.
@protected @protected
...@@ -140,6 +160,31 @@ abstract class Route<T> { ...@@ -140,6 +160,31 @@ abstract class Route<T> {
}); });
} }
/// Called after [install] when the route is added to the navigator.
///
/// This method is called instead of [didPush] when the route immediately
/// appears on screen without any push transition.
///
/// The [didChangeNext] and [didChangePrevious] methods are typically called
/// immediately after this method is called.
@protected
@mustCallSuper
void didAdd() {
// This TickerFuture serves two purposes. First, we want to make sure
// animations triggered by other operations finish before focusing the
// navigator. Second, navigator.focusScopeNode might acquire more focused
// children in Route.install asynchronously. This TickerFuture will wait for
// it to finish first.
//
// The later case can be found when subclasses manage their own focus scopes.
// For example, ModalRoute create a focus scope in its overlay entries. The
// focused child can only be attached to navigator after initState which
// will be guarded by the asynchronous gap.
TickerFuture.complete()..then<void>((void _) {
navigator.focusScopeNode.requestFocus();
});
}
/// Called after [install] when the route replaced another in the navigator. /// Called after [install] when the route replaced another in the navigator.
/// ///
/// The [didChangeNext] and [didChangePrevious] methods are typically called /// The [didChangeNext] and [didChangePrevious] methods are typically called
...@@ -148,12 +193,24 @@ abstract class Route<T> { ...@@ -148,12 +193,24 @@ abstract class Route<T> {
@mustCallSuper @mustCallSuper
void didReplace(Route<dynamic> oldRoute) { } void didReplace(Route<dynamic> oldRoute) { }
/// Returns whether this route wants to veto a [Navigator.pop]. This method is /// Returns whether calling [Navigator.maybePop] when this [Route] is current
/// called by [Navigator.maybePop]. /// ([isCurrent]) should do anything.
///
/// [Navigator.maybePop] is usually used instead of [pop] to handle the system
/// back button.
/// ///
/// By default, routes veto a pop if they're the first route in the history /// By default, if a [Route] is the first route in the history (i.e., if
/// (i.e., if [isFirst]). This behavior prevents the user from popping the /// [isFirst]), it reports that pops should be bubbled
/// first route off the history and being stranded at a blank screen. /// ([RoutePopDisposition.bubble]). This behavior prevents the user from
/// popping the first route off the history and being stranded at a blank
/// screen; instead, the larger scope is popped (e.g. the application quits,
/// so that the user returns to the previous application).
///
/// In other cases, the default behaviour is to accept the pop
/// ([RoutePopDisposition.pop]).
///
/// The third possible value is [RoutePopDisposition.doNotPop], which causes
/// the pop request to be ignored entirely.
/// ///
/// See also: /// See also:
/// ///
...@@ -170,17 +227,22 @@ abstract class Route<T> { ...@@ -170,17 +227,22 @@ abstract class Route<T> {
/// When this route is popped (see [Navigator.pop]) if the result isn't /// When this route is popped (see [Navigator.pop]) if the result isn't
/// specified or if it's null, this value will be used instead. /// specified or if it's null, this value will be used instead.
///
/// This fallback is implemented by [didComplete]. This value is used if the
/// argument to that method is null.
T get currentResult => null; T get currentResult => null;
/// A future that completes when this route is popped off the navigator. /// A future that completes when this route is popped off the navigator.
/// ///
/// The future completes with the value given to [Navigator.pop], if any. /// The future completes with the value given to [Navigator.pop], if any, or
/// else the value of [currentResult]. See [didComplete] for more discussion
/// on this topic.
Future<T> get popped => _popCompleter.future; Future<T> get popped => _popCompleter.future;
final Completer<T> _popCompleter = Completer<T>(); final Completer<T> _popCompleter = Completer<T>();
/// A request was made to pop this route. If the route can handle it /// A request was made to pop this route. If the route can handle it
/// internally (e.g. because it has its own stack of internal state) then /// internally (e.g. because it has its own stack of internal state) then
/// return false, otherwise return true (by return the value of calling /// return false, otherwise return true (by returning the value of calling
/// `super.didPop`). Returning false will prevent the default behavior of /// `super.didPop`). Returning false will prevent the default behavior of
/// [NavigatorState.pop]. /// [NavigatorState.pop].
/// ///
...@@ -190,6 +252,13 @@ abstract class Route<T> { ...@@ -190,6 +252,13 @@ abstract class Route<T> {
/// call [dispose] on the route. This sequence lets the route perform an /// call [dispose] on the route. This sequence lets the route perform an
/// exit animation (or some other visual effect) after being popped but prior /// exit animation (or some other visual effect) after being popped but prior
/// to being disposed. /// to being disposed.
///
/// This method should call [didComplete] to resolve the [popped] future (and
/// this is all that the default implementation does); routes should not wait
/// for their exit animation to complete before doing so.
///
/// See [popped], [didComplete], and [currentResult] for a discussion of the
/// `result` argument.
@protected @protected
@mustCallSuper @mustCallSuper
bool didPop(T result) { bool didPop(T result) {
...@@ -199,34 +268,56 @@ abstract class Route<T> { ...@@ -199,34 +268,56 @@ abstract class Route<T> {
/// The route was popped or is otherwise being removed somewhat gracefully. /// The route was popped or is otherwise being removed somewhat gracefully.
/// ///
/// This is called by [didPop] and in response to [Navigator.pushReplacement]. /// This is called by [didPop] and in response to
/// [NavigatorState.pushReplacement]. If [didPop] was not called, then the
/// [Navigator.finalizeRoute] method must be called immediately, and no exit
/// animation will run.
///
/// The [popped] future is completed by this method. The `result` argument
/// specifies the value that this future is completed with, unless it is null,
/// in which case [currentResult] is used instead.
/// ///
/// The [popped] future is completed by this method. /// This should be called before the pop animation, if any, takes place,
/// though in some cases the animation may be driven by the user before the
/// route is committed to being popped; this can in particular happen with the
/// iOS-style back gesture. See [Navigator.didStartUserGesture].
@protected @protected
@mustCallSuper @mustCallSuper
void didComplete(T result) { void didComplete(T result) {
_popCompleter.complete(result); _popCompleter.complete(result ?? currentResult);
} }
/// The given route, which was above this one, has been popped off the /// The given route, which was above this one, has been popped off the
/// navigator. /// navigator.
///
/// This route is now the current route ([isCurrent] is now true), and there
/// is no next route.
@protected @protected
@mustCallSuper @mustCallSuper
void didPopNext(Route<dynamic> nextRoute) { } void didPopNext(Route<dynamic> nextRoute) { }
/// This route's next route has changed to the given new route. This is called /// This route's next route has changed to the given new route.
/// on a route whenever the next route changes for any reason, so long as it ///
/// is in the history, including when a route is first added to a [Navigator] /// This is called on a route whenever the next route changes for any reason,
/// (e.g. by [Navigator.push]), except for cases when [didPopNext] would be /// so long as it is in the history, including when a route is first added to
/// called. `nextRoute` will be null if there's no next route. /// a [Navigator] (e.g. by [Navigator.push]), except for cases when
/// [didPopNext] would be called.
///
/// The `nextRoute` argument will be null if there's no new next route (i.e.
/// if [isCurrent] is true).
@protected @protected
@mustCallSuper @mustCallSuper
void didChangeNext(Route<dynamic> nextRoute) { } void didChangeNext(Route<dynamic> nextRoute) { }
/// This route's previous route has changed to the given new route. This is /// This route's previous route has changed to the given new route.
/// called on a route whenever the previous route changes for any reason, so ///
/// long as it is in the history. `previousRoute` will be null if there's no /// This is called on a route whenever the previous route changes for any
/// previous route. /// reason, so long as it is in the history, except for immediately after the
/// route itself has been pushed (in which case [didPush] or [didReplace] will
/// be called instead).
///
/// The `previousRoute` argument will be null if there's no previous route
/// (i.e. if [isFirst] is true).
@protected @protected
@mustCallSuper @mustCallSuper
void didChangePrevious(Route<dynamic> previousRoute) { } void didChangePrevious(Route<dynamic> previousRoute) { }
...@@ -263,9 +354,17 @@ abstract class Route<T> { ...@@ -263,9 +354,17 @@ abstract class Route<T> {
@mustCallSuper @mustCallSuper
void changedExternalState() { } void changedExternalState() { }
/// The route should remove its overlays and free any other resources. /// Discards any resources used by the object.
///
/// This method should not remove its [overlayEntries] from the [Overlay]. The
/// object's owner is in charge of doing that.
/// ///
/// This route is no longer referenced by the navigator. /// After this is called, the object is not in a usable state and should be
/// discarded.
///
/// This method should only be called by the object's owner; typically the
/// [Navigator] owns a route and so will call this method when the route is
/// removed, after which the route is no longer referenced by the navigator.
@mustCallSuper @mustCallSuper
@protected @protected
void dispose() { void dispose() {
...@@ -276,7 +375,15 @@ abstract class Route<T> { ...@@ -276,7 +375,15 @@ abstract class Route<T> {
/// ///
/// If this is true, then [isActive] is also true. /// If this is true, then [isActive] is also true.
bool get isCurrent { bool get isCurrent {
return _navigator != null && _navigator._history.last == this; if (_navigator == null)
return false;
final _RouteEntry currentRouteEntry = _navigator._history.lastWhere(
_RouteEntry.isPresentPredicate,
orElse: () => null,
);
if (currentRouteEntry == null)
return false;
return currentRouteEntry.route == this;
} }
/// Whether this route is the bottom-most route on the navigator. /// Whether this route is the bottom-most route on the navigator.
...@@ -287,7 +394,15 @@ abstract class Route<T> { ...@@ -287,7 +394,15 @@ abstract class Route<T> {
/// If [isFirst] and [isCurrent] are both true then this is the only route on /// If [isFirst] and [isCurrent] are both true then this is the only route on
/// the navigator (and [isActive] will also be true). /// the navigator (and [isActive] will also be true).
bool get isFirst { bool get isFirst {
return _navigator != null && _navigator._history.first == this; if (_navigator == null)
return false;
final _RouteEntry currentRouteEntry = _navigator._history.firstWhere(
_RouteEntry.isPresentPredicate,
orElse: () => null,
);
if (currentRouteEntry == null)
return false;
return currentRouteEntry.route == this;
} }
/// Whether this route is on the navigator. /// Whether this route is on the navigator.
...@@ -300,7 +415,12 @@ abstract class Route<T> { ...@@ -300,7 +415,12 @@ abstract class Route<T> {
/// rendered. It is even possible for the route to be active but for the stateful /// rendered. It is even possible for the route to be active but for the stateful
/// widgets within the route to not be instantiated. See [ModalRoute.maintainState]. /// widgets within the route to not be instantiated. See [ModalRoute.maintainState].
bool get isActive { bool get isActive {
return _navigator != null && _navigator._history.contains(this); if (_navigator == null)
return false;
return _navigator._history.firstWhere(
_RouteEntry.isRoutePredicate(this),
orElse: () => null,
)?.isPresent == true;
} }
} }
...@@ -318,12 +438,10 @@ class RouteSettings { ...@@ -318,12 +438,10 @@ class RouteSettings {
/// replaced with the new values. /// replaced with the new values.
RouteSettings copyWith({ RouteSettings copyWith({
String name, String name,
bool isInitialRoute,
Object arguments, Object arguments,
}) { }) {
return RouteSettings( return RouteSettings(
name: name ?? this.name, name: name ?? this.name,
isInitialRoute: isInitialRoute ?? this.isInitialRoute,
arguments: arguments ?? this.arguments, arguments: arguments ?? this.arguments,
); );
} }
...@@ -336,6 +454,14 @@ class RouteSettings { ...@@ -336,6 +454,14 @@ class RouteSettings {
/// Whether this route is the very first route being pushed onto this [Navigator]. /// Whether this route is the very first route being pushed onto this [Navigator].
/// ///
/// The initial route typically skips any entrance transition to speed startup. /// The initial route typically skips any entrance transition to speed startup.
///
/// This property has been deprecated. Uses [Navigator.onGenerateInitialRoutes]
/// to customize initial routes instead. This feature was deprecated after
/// v1.14.1.
@Deprecated(
'Uses onGenerateInitialRoutes to customize initial routes instead. '
'This feature was deprecated after v1.14.1.'
)
final bool isInitialRoute; final bool isInitialRoute;
/// The arguments passed to this route. /// The arguments passed to this route.
...@@ -379,17 +505,10 @@ class NavigatorObserver { ...@@ -379,17 +505,10 @@ class NavigatorObserver {
/// The [Navigator] replaced `oldRoute` with `newRoute`. /// The [Navigator] replaced `oldRoute` with `newRoute`.
void didReplace({ Route<dynamic> newRoute, Route<dynamic> oldRoute }) { } void didReplace({ Route<dynamic> newRoute, Route<dynamic> oldRoute }) { }
/// The [Navigator]'s route `route` is being moved by a user gesture. /// The [Navigator]'s routes are being moved by a user gesture.
///
/// For example, this is called when an iOS back gesture starts.
/// ///
/// Paired with a call to [didStopUserGesture] when the route is no longer /// For example, this is called when an iOS back gesture starts, and is used
/// being manipulated via user gesture. /// to disabled hero animations during such interactions.
///
/// If present, the route immediately below `route` is `previousRoute`.
/// Though the gesture may not necessarily conclude at `previousRoute` if
/// the gesture is canceled. In that case, [didStopUserGesture] is still
/// called but a follow-up [didPop] is not.
void didStartUserGesture(Route<dynamic> route, Route<dynamic> previousRoute) { } void didStartUserGesture(Route<dynamic> route, Route<dynamic> previousRoute) { }
/// User gesture is no longer controlling the [Navigator]. /// User gesture is no longer controlling the [Navigator].
...@@ -407,7 +526,7 @@ class NavigatorObserver { ...@@ -407,7 +526,7 @@ class NavigatorObserver {
/// around in the overlay. Similarly, the navigator can be used to show a dialog /// around in the overlay. Similarly, the navigator can be used to show a dialog
/// by positioning the dialog widget above the current page. /// by positioning the dialog widget above the current page.
/// ///
/// ## Using the Navigator /// ## Using the Navigator API
/// ///
/// 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
...@@ -427,9 +546,10 @@ class NavigatorObserver { ...@@ -427,9 +546,10 @@ class NavigatorObserver {
/// ///
/// ### Displaying a full-screen route /// ### Displaying a full-screen route
/// ///
/// Although you can create a navigator directly, it's most common to use /// Although you can create a navigator directly, it's most common to use the
/// the navigator created by a [WidgetsApp] or a [MaterialApp] widget. You /// navigator created by the [Router] which itself is created and configured by
/// can refer to that navigator with [Navigator.of]. /// a [WidgetsApp] or a [MaterialApp] widget. You can refer to that navigator
/// with [Navigator.of].
/// ///
/// A [MaterialApp] is the simplest way to set things up. The [MaterialApp]'s /// A [MaterialApp] is the simplest way to set things up. The [MaterialApp]'s
/// home becomes the route at the bottom of the [Navigator]'s stack. It is what /// home becomes the route at the bottom of the [Navigator]'s stack. It is what
...@@ -602,12 +722,12 @@ class NavigatorObserver { ...@@ -602,12 +722,12 @@ class NavigatorObserver {
/// ///
/// ### Nesting Navigators /// ### Nesting Navigators
/// ///
/// An app can use more than one Navigator. Nesting one Navigator below /// An app can use more than one [Navigator]. Nesting one [Navigator] below
/// another Navigator can be used to create an "inner journey" such as tabbed /// another [Navigator] can be used to create an "inner journey" such as tabbed
/// navigation, user registration, store checkout, or other independent journeys /// navigation, user registration, store checkout, or other independent journeys
/// that represent a subsection of your overall application. /// that represent a subsection of your overall application.
/// ///
/// #### Real World Example /// #### Example
/// ///
/// It is standard practice for iOS apps to use tabbed navigation where each /// It is standard practice for iOS apps to use tabbed navigation where each
/// tab maintains its own navigation history. Therefore, each tab has its own /// tab maintains its own navigation history. Therefore, each tab has its own
...@@ -620,10 +740,9 @@ class NavigatorObserver { ...@@ -620,10 +740,9 @@ class NavigatorObserver {
/// tab's [Navigator]s are actually nested [Navigator]s sitting below a single /// tab's [Navigator]s are actually nested [Navigator]s sitting below a single
/// root [Navigator]. /// root [Navigator].
/// ///
/// The nested [Navigator]s for tabbed navigation sit in [WidgetApp] and /// In practice, the nested [Navigator]s for tabbed navigation sit in the
/// [CupertinoTabView], so you don't need to worry about nested [Navigator]s /// [WidgetApp] and [CupertinoTabView] widgets and do not need to be explicitly
/// in this situation, but it's a real world example where nested [Navigator]s /// created or managed.
/// are used.
/// ///
/// {@tool sample --template=freeform} /// {@tool sample --template=freeform}
/// The following example demonstrates how a nested [Navigator] can be used to /// The following example demonstrates how a nested [Navigator] can be used to
...@@ -770,33 +889,19 @@ class Navigator extends StatefulWidget { ...@@ -770,33 +889,19 @@ class Navigator extends StatefulWidget {
const Navigator({ const Navigator({
Key key, Key key,
this.initialRoute, this.initialRoute,
@required this.onGenerateRoute, this.onGenerateInitialRoutes = Navigator.defaultGenerateInitialRoutes,
this.onGenerateRoute,
this.onUnknownRoute, this.onUnknownRoute,
this.observers = const <NavigatorObserver>[], this.observers = const <NavigatorObserver>[],
}) : assert(onGenerateRoute != null), }) : assert(onGenerateInitialRoutes != null),
super(key: key); super(key: key);
/// The name of the first route to show. /// The name of the first route to show.
/// ///
/// By default, this defers to [dart:ui.Window.defaultRouteName]. /// Defaults to [Navigator.defaultRouteName].
/// ///
/// If this string contains any `/` characters, then the string is split on /// The value is interpreted according to [onGenerateInitialRoutes], which
/// those characters and substrings from the start of the string up to each /// defaults to [defaultGenerateInitialRoutes].
/// such character are, in turn, used as routes to push.
///
/// For example, if the route `/stocks/HOOLI` was used as the [initialRoute],
/// then the [Navigator] would push the following routes on startup: `/`,
/// `/stocks`, `/stocks/HOOLI`. This enables deep linking while allowing the
/// application to maintain a predictable route history.
///
/// If any of the intermediate routes doesn't exist, it'll simply be skipped.
/// In the example above, if `/stocks` doesn't have a corresponding route in
/// the app, it'll be skipped and only `/` and `/stocks/HOOLI` will be pushed.
///
/// That said, the full route has to map to something in the app in order for
/// this to work. In our example, `/stocks/HOOLI` has to map to a route in the
/// app. Otherwise, [initialRoute] will be ignored and [defaultRouteName] will
/// be used instead.
final String initialRoute; final String initialRoute;
/// Called to generate a route for a given [RouteSettings]. /// Called to generate a route for a given [RouteSettings].
...@@ -815,7 +920,7 @@ class Navigator extends StatefulWidget { ...@@ -815,7 +920,7 @@ class Navigator extends StatefulWidget {
/// A list of observers for this navigator. /// A list of observers for this navigator.
final List<NavigatorObserver> observers; final List<NavigatorObserver> observers;
/// The default name for the [initialRoute]. /// The name for the default route of the application.
/// ///
/// See also: /// See also:
/// ///
...@@ -823,6 +928,16 @@ class Navigator extends StatefulWidget { ...@@ -823,6 +928,16 @@ class Navigator extends StatefulWidget {
/// application was started with. /// application was started with.
static const String defaultRouteName = '/'; static const String defaultRouteName = '/';
/// Called when the widget is created to generate the initial list of [Route]
/// objects if [initialRoute] is not null.
///
/// Defaults to [defaultGenerateInitialRoutes].
///
/// The [NavigatorState] and [initialRoute] will be passed to the callback.
/// The callback must return a list of [Route] objects with which the history
/// will be primed.
final RouteListFactory onGenerateInitialRoutes;
/// Push a named route onto the navigator that most tightly encloses the given /// Push a named route onto the navigator that most tightly encloses the given
/// context. /// context.
/// ///
...@@ -842,6 +957,8 @@ class Navigator extends StatefulWidget { ...@@ -842,6 +957,8 @@ class Navigator extends StatefulWidget {
/// when the pushed route is popped off the navigator. /// when the pushed route is popped off the navigator.
/// ///
/// The `T` type argument is the type of the return value of the route. /// The `T` type argument is the type of the return value of the route.
///
/// To use [pushNamed], an [onGenerateRoute] callback must be provided,
/// {@endtemplate} /// {@endtemplate}
/// ///
/// {@template flutter.widgets.navigator.pushNamed.arguments} /// {@template flutter.widgets.navigator.pushNamed.arguments}
...@@ -948,6 +1065,9 @@ class Navigator extends StatefulWidget { ...@@ -948,6 +1065,9 @@ class Navigator extends StatefulWidget {
/// ///
/// The `T` type argument is the type of the return value of the new route, /// The `T` type argument is the type of the return value of the new route,
/// and `TO` is the type of the return value of the old route. /// and `TO` is the type of the return value of the old route.
///
/// To use [pushReplacementNamed], an [onGenerateRoute] callback must be
/// provided.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// {@macro flutter.widgets.navigator.pushNamed.arguments} /// {@macro flutter.widgets.navigator.pushNamed.arguments}
...@@ -976,14 +1096,9 @@ class Navigator extends StatefulWidget { ...@@ -976,14 +1096,9 @@ class Navigator extends StatefulWidget {
/// given context and push a named route in its place. /// given context and push a named route in its place.
/// ///
/// {@template flutter.widgets.navigator.popAndPushNamed} /// {@template flutter.widgets.navigator.popAndPushNamed}
/// If non-null, `result` will be used as the result of the route that is /// The popping of the previous route is handled as per [pop].
/// popped; the future that had been returned from pushing the popped route
/// will complete with `result`. Routes such as dialogs or popup menus
/// typically use this mechanism to return the value selected by the user to
/// the widget that created their route. The type of `result`, if provided,
/// must match the type argument of the class of the popped route (`TO`).
/// ///
/// The route name will be passed to the navigator's [onGenerateRoute] /// The new route's name will be passed to the navigator's [onGenerateRoute]
/// callback. The returned route will be pushed into the navigator. /// callback. The returned route will be pushed into the navigator.
/// ///
/// The new route, the old route, and the route below the old route (if any) /// The new route, the old route, and the route below the old route (if any)
...@@ -1003,6 +1118,9 @@ class Navigator extends StatefulWidget { ...@@ -1003,6 +1118,9 @@ class Navigator extends StatefulWidget {
/// ///
/// The `T` type argument is the type of the return value of the new route, /// The `T` type argument is the type of the return value of the new route,
/// and `TO` is the return value type of the old route. /// and `TO` is the return value type of the old route.
///
/// To use [popAndPushNamed], an [onGenerateRoute] callback must be provided.
///
/// {@endtemplate} /// {@endtemplate}
/// ///
/// {@macro flutter.widgets.navigator.pushNamed.arguments} /// {@macro flutter.widgets.navigator.pushNamed.arguments}
...@@ -1064,6 +1182,9 @@ class Navigator extends StatefulWidget { ...@@ -1064,6 +1182,9 @@ class Navigator extends StatefulWidget {
/// when the pushed route is popped off the navigator. /// when the pushed route is popped off the navigator.
/// ///
/// The `T` type argument is the type of the return value of the new route. /// The `T` type argument is the type of the return value of the new route.
///
/// To use [pushNamedAndRemoveUntil], an [onGenerateRoute] callback must be
/// provided.
/// {@endtemplate} /// {@endtemplate}
/// ///
/// {@macro flutter.widgets.navigator.pushNamed.arguments} /// {@macro flutter.widgets.navigator.pushNamed.arguments}
...@@ -1309,21 +1430,28 @@ class Navigator extends StatefulWidget { ...@@ -1309,21 +1430,28 @@ class Navigator extends StatefulWidget {
return navigator != null && navigator.canPop(); return navigator != null && navigator.canPop();
} }
/// Tries to pop the current route of the navigator that most tightly encloses /// Consults the current route's [Route.willPop] method, and acts accordingly,
/// the given context, while honoring the route's [Route.willPop] /// potentially popping the route as a result; returns whether the pop request
/// state. /// should be considered handled.
/// ///
/// {@template flutter.widgets.navigator.maybePop} /// {@template flutter.widgets.navigator.maybePop}
/// Returns false if the route deferred to the next enclosing navigator /// If [Route.willPop] returns [RoutePopDisposition.pop], then the [pop]
/// (possibly the system); otherwise, returns true (whether the route was /// method is called, and this method returns true, indicating that it handled
/// popped or not). /// the pop request.
///
/// If [Route.willPop] returns [RoutePopDisposition.doNotPop], then this
/// method returns true, but does not do anything beyond that.
/// ///
/// This method is typically called to handle a user-initiated [pop]. For /// If [Route.willPop] returns [RoutePopDisposition.bubble], then this method
/// example on Android it's called by the binding for the system's back /// returns false, and the caller is responsible for sending the request to
/// button. /// the containing scope (e.g. by closing the application).
///
/// This method is typically called for a user-initiated [pop]. For example on
/// Android it's called by the binding for the system's back button.
/// ///
/// The `T` type argument is the type of the return value of the current /// The `T` type argument is the type of the return value of the current
/// route. /// route. (Typically this isn't known; consider specifying `dynamic` or
/// `Null`.)
/// {@endtemplate} /// {@endtemplate}
/// ///
/// See also: /// See also:
...@@ -1342,8 +1470,8 @@ class Navigator extends StatefulWidget { ...@@ -1342,8 +1470,8 @@ class Navigator extends StatefulWidget {
/// ///
/// {@template flutter.widgets.navigator.pop} /// {@template flutter.widgets.navigator.pop}
/// The current route's [Route.didPop] method is called first. If that method /// The current route's [Route.didPop] method is called first. If that method
/// returns false, then this method returns true but nothing else is changed /// returns false, then the route remains in the [Navigator]'s history (the
/// (the route is expected to have popped some internal state; see e.g. /// route is expected to have popped some internal state; see e.g.
/// [LocalHistoryRoute]). Otherwise, the rest of this description applies. /// [LocalHistoryRoute]). Otherwise, the rest of this description applies.
/// ///
/// If non-null, `result` will be used as the result of the route that is /// If non-null, `result` will be used as the result of the route that is
...@@ -1360,8 +1488,8 @@ class Navigator extends StatefulWidget { ...@@ -1360,8 +1488,8 @@ class Navigator extends StatefulWidget {
/// ///
/// The `T` type argument is the type of the return value of the popped route. /// The `T` type argument is the type of the return value of the popped route.
/// ///
/// Returns true if a route was popped (including if [Route.didPop] returned /// The type of `result`, if provided, must match the type argument of the
/// false); returns false if there are no further previous routes. /// class of the popped route (`T`).
/// {@endtemplate} /// {@endtemplate}
/// ///
/// {@tool snippet} /// {@tool snippet}
...@@ -1383,8 +1511,8 @@ class Navigator extends StatefulWidget { ...@@ -1383,8 +1511,8 @@ class Navigator extends StatefulWidget {
/// } /// }
/// ``` /// ```
@optionalTypeArgs @optionalTypeArgs
static bool pop<T extends Object>(BuildContext context, [ T result ]) { static void pop<T extends Object>(BuildContext context, [ T result ]) {
return Navigator.of(context).pop<T>(result); Navigator.of(context).pop<T>(result);
} }
/// Calls [pop] repeatedly on the navigator that most tightly encloses the /// Calls [pop] repeatedly on the navigator that most tightly encloses the
...@@ -1503,44 +1631,43 @@ class Navigator extends StatefulWidget { ...@@ -1503,44 +1631,43 @@ class Navigator extends StatefulWidget {
return navigator; return navigator;
} }
@override /// Turn a route name into a set of [Route] objects.
NavigatorState createState() => NavigatorState(); ///
} /// This is the default value of [onGenerateInitialRoutes], which is used if
/// [initialRoute] is not null.
/// The state for a [Navigator] widget. ///
class NavigatorState extends State<Navigator> with TickerProviderStateMixin { /// If this string contains any `/` characters, then the string is split on
final GlobalKey<OverlayState> _overlayKey = GlobalKey<OverlayState>(); /// those characters and substrings from the start of the string up to each
final List<Route<dynamic>> _history = <Route<dynamic>>[]; /// such character are, in turn, used as routes to push.
final Set<Route<dynamic>> _poppedRoutes = <Route<dynamic>>{}; ///
/// For example, if the route `/stocks/HOOLI` was used as the [initialRoute],
/// The [FocusScopeNode] for the [FocusScope] that encloses the routes. /// then the [Navigator] would push the following routes on startup: `/`,
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope'); /// `/stocks`, `/stocks/HOOLI`. This enables deep linking while allowing the
/// application to maintain a predictable route history.
final List<OverlayEntry> _initialOverlayEntries = <OverlayEntry>[]; static List<Route<dynamic>> defaultGenerateInitialRoutes(NavigatorState navigator, String initialRouteName) {
final List<Route<dynamic>> result = <Route<dynamic>>[];
@override
void initState() {
super.initState();
for (final NavigatorObserver observer in widget.observers) {
assert(observer.navigator == null);
observer._navigator = this;
}
String initialRouteName = widget.initialRoute ?? Navigator.defaultRouteName;
if (initialRouteName.startsWith('/') && initialRouteName.length > 1) { if (initialRouteName.startsWith('/') && initialRouteName.length > 1) {
initialRouteName = initialRouteName.substring(1); // strip leading '/' initialRouteName = initialRouteName.substring(1); // strip leading '/'
assert(Navigator.defaultRouteName == '/'); assert(Navigator.defaultRouteName == '/');
final List<Route<dynamic>> plannedInitialRoutes = <Route<dynamic>>[ List<String> debugRouteNames;
_routeNamed<dynamic>(Navigator.defaultRouteName, allowNull: true, arguments: null), assert(() {
]; debugRouteNames = <String>[ Navigator.defaultRouteName ];
return true;
}());
result.add(navigator._routeNamed<dynamic>(Navigator.defaultRouteName, arguments: null, allowNull: true));
final List<String> routeParts = initialRouteName.split('/'); final List<String> routeParts = initialRouteName.split('/');
if (initialRouteName.isNotEmpty) { if (initialRouteName.isNotEmpty) {
String routeName = ''; String routeName = '';
for (final String part in routeParts) { for (final String part in routeParts) {
routeName += '/$part'; routeName += '/$part';
plannedInitialRoutes.add(_routeNamed<dynamic>(routeName, allowNull: true, arguments: null)); assert(() {
debugRouteNames.add(routeName);
return true;
}());
result.add(navigator._routeNamed<dynamic>(routeName, arguments: null, allowNull: true));
} }
} }
if (plannedInitialRoutes.last == null) { if (result.last == null) {
assert(() { assert(() {
FlutterError.reportError( FlutterError.reportError(
FlutterErrorDetails( FlutterErrorDetails(
...@@ -1553,19 +1680,293 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1553,19 +1680,293 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
); );
return true; return true;
}()); }());
push(_routeNamed<Object>(Navigator.defaultRouteName, arguments: null)); result.clear();
} else {
plannedInitialRoutes.where((Route<dynamic> route) => route != null).forEach(push);
} }
} else if (initialRouteName != Navigator.defaultRouteName) {
// If initialRouteName wasn't '/', then we try to get it with allowNull:true, so that if that fails,
// we fall back to '/' (without allowNull:true, see below).
result.add(navigator._routeNamed<dynamic>(initialRouteName, arguments: null, allowNull: true));
}
// Null route might be a result of gap in initialRouteName
//
// For example, routes = ['A', 'A/B/C'], and initialRouteName = 'A/B/C'
// This should result in result = ['A', null,'A/B/C'] where 'A/B' produces
// the null. In this case, we want to filter out the null and return
// result = ['A', 'A/B/C'].
result.removeWhere((Route<dynamic> route) => route == null);
if (result.isEmpty)
result.add(navigator._routeNamed<dynamic>(Navigator.defaultRouteName, arguments: null));
return result;
}
@override
NavigatorState createState() => NavigatorState();
}
// The _RouteLifecycle state machine (only goes down):
//
// [creation of a _RouteEntry]
// / | | |
// / | | |
// / | | |
// / | | |
// / | | |
// pushReplace push* add* replace*
// \ | | |
// \ | | /
// +--pushing# | /
// \ / /
// \ / /
// idle--+-----+
// / \
// / \
// pop* remove*
// / \
// / removing#
// popping# |
// | |
// [finalizeRoute] |
// \ |
// dispose*
// |
// |
// disposed
// |
// |
// [_RouteEntry garbage collected]
// (terminal state)
//
// * These states are transient; as soon as _flushHistoryUpdates is run the
// route entry will exit that state.
// # These states await futures or other events, then transition automatically.
enum _RouteLifecycle {
// routes that are and will be present:
add, // we'll want to run install, didAdd, etc; a route created by onGenerateInitialRoutes or by the initial widget.pages
push, // we'll want to run install, didPush, etc; a route added via push() and friends
pushReplace, // we'll want to run install, didPush, etc; a route added via pushReplace() and friends
pushing, // we're waiting for the future from didPush to complete
replace, // we'll want to run install, didReplace, etc; a route added via replace() and friends
idle, // route is being harmless
// routes that are but will not present:
pop, // we'll want to call didPop
remove, // we'll want to run didReplace/didRemove etc
// routes that are not and will not present:
popping, // we're waiting for the route to call finalizeRoute to switch to dispose
removing, // we are waiting for subsequent routes to be done animating, then will switch to dispose
dispose, // we will dispose the route momentarily
disposed, // we have disposed the route
}
typedef _RouteEntryPredicate = bool Function(_RouteEntry entry);
class _RouteEntry {
_RouteEntry(
this.route, {
@required _RouteLifecycle initialState,
}) : assert(route != null),
assert(initialState != null),
assert(
initialState == _RouteLifecycle.add ||
initialState == _RouteLifecycle.push ||
initialState == _RouteLifecycle.pushReplace ||
initialState == _RouteLifecycle.replace
),
currentState = initialState; // ignore: prefer_initializing_formals
final Route<dynamic> route;
_RouteLifecycle currentState;
Route<dynamic> lastAnnouncedNextRoute; // last argument to Route.didChangeNext
Route<dynamic> lastAnnouncedPreviousRoute; // last argument to Route.didChangePrevious
Route<dynamic> lastAnnouncedPoppedNextRoute; // last argument to Route.didPopNext
void handleAdd({ @required NavigatorState navigator, @required bool isNewFirst, @required Route<dynamic> previous, @required Route<dynamic> previousPresent }) {
assert(currentState == _RouteLifecycle.add);
assert(navigator != null);
assert(navigator._debugLocked);
assert(route._navigator == null);
route._navigator = navigator;
route.install();
assert(route.overlayEntries.isNotEmpty);
route.didAdd();
currentState = _RouteLifecycle.idle;
if (isNewFirst) {
route.didChangeNext(null);
}
RouteNotificationMessages.maybeNotifyRouteChange(_routePushedMethod, route, previous);
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 }) {
assert(currentState == _RouteLifecycle.push || currentState == _RouteLifecycle.pushReplace || currentState == _RouteLifecycle.replace);
assert(navigator != null);
assert(navigator._debugLocked);
assert(route._navigator == null);
final _RouteLifecycle previousState = currentState;
route._navigator = navigator;
route.install();
assert(route.overlayEntries.isNotEmpty);
if (currentState == _RouteLifecycle.push || currentState == _RouteLifecycle.pushReplace) {
final TickerFuture routeFuture = route.didPush();
currentState = _RouteLifecycle.pushing;
routeFuture.whenCompleteOrCancel(() {
if (currentState == _RouteLifecycle.pushing) {
currentState = _RouteLifecycle.idle;
assert(!navigator._debugLocked);
assert(() { navigator._debugLocked = true; return true; }());
navigator._flushHistoryUpdates();
assert(() { navigator._debugLocked = false; return true; }());
}
});
} else { } else {
Route<Object> route; assert(currentState == _RouteLifecycle.replace);
if (initialRouteName != Navigator.defaultRouteName) route.didReplace(previous);
route = _routeNamed<Object>(initialRouteName, allowNull: true, arguments: null); currentState = _RouteLifecycle.idle;
route ??= _routeNamed<Object>(Navigator.defaultRouteName, arguments: null); }
push(route); if (isNewFirst) {
route.didChangeNext(null);
} }
for (final Route<dynamic> route in _history)
_initialOverlayEntries.addAll(route.overlayEntries); if (previousState == _RouteLifecycle.replace || previousState == _RouteLifecycle.pushReplace) {
RouteNotificationMessages.maybeNotifyRouteChange(_routeReplacedMethod, route, previous);
for (final NavigatorObserver observer in navigator.widget.observers)
observer.didReplace(newRoute: route, oldRoute: previous);
} else {
assert(previousState == _RouteLifecycle.push);
RouteNotificationMessages.maybeNotifyRouteChange(_routePushedMethod, route, previous);
for (final NavigatorObserver observer in navigator.widget.observers)
observer.didPush(route, previousPresent);
}
}
void handleDidPopNext(Route<dynamic> poppedRoute) {
route.didPopNext(poppedRoute);
lastAnnouncedPoppedNextRoute = poppedRoute;
}
void handlePop({ @required NavigatorState navigator, @required Route<dynamic> previousPresent }) {
assert(navigator != null);
assert(navigator._debugLocked);
assert(route._navigator == navigator);
currentState = _RouteLifecycle.popping;
for (final NavigatorObserver observer in navigator.widget.observers)
observer.didPop(route, previousPresent);
RouteNotificationMessages.maybeNotifyRouteChange(
_routePoppedMethod,
route,
previousPresent,
);
}
void handleRemoval({ @required NavigatorState navigator, @required Route<dynamic> previousPresent }) {
assert(navigator != null);
assert(navigator._debugLocked);
assert(route._navigator == navigator);
currentState = _RouteLifecycle.removing;
if (_reportRemovalToObserver) {
for (final NavigatorObserver observer in navigator.widget.observers)
observer.didRemove(route, previousPresent);
}
}
bool doingPop = false;
void pop<T>(T result) {
assert(isPresent);
doingPop = true;
if (route.didPop(result) && doingPop) {
currentState = _RouteLifecycle.pop;
}
doingPop = false;
}
bool _reportRemovalToObserver = true;
// Route is removed without being completed.
void remove({ bool isReplaced = false }) {
if (currentState.index >= _RouteLifecycle.remove.index)
return;
assert(isPresent);
_reportRemovalToObserver = !isReplaced;
currentState = _RouteLifecycle.remove;
}
// Route completes with `result` and is removed.
void complete<T>(T result, { bool isReplaced = false }) {
if (currentState.index >= _RouteLifecycle.remove.index)
return;
assert(isPresent);
_reportRemovalToObserver = !isReplaced;
route.didComplete(result);
assert(route._popCompleter.isCompleted); // implies didComplete was called
currentState = _RouteLifecycle.remove;
}
void finalize() {
assert(currentState.index < _RouteLifecycle.dispose.index);
currentState = _RouteLifecycle.dispose;
}
void dispose() {
assert(currentState.index < _RouteLifecycle.disposed.index);
route.dispose();
currentState = _RouteLifecycle.disposed;
}
bool get willBePresent => currentState.index <= _RouteLifecycle.idle.index;
bool get isPresent => currentState.index <= _RouteLifecycle.remove.index;
bool shouldAnnounceChangeToNext(Route<dynamic> nextRoute) {
assert(nextRoute != lastAnnouncedNextRoute);
// Do not announce if `next` changes from a just popped route to null. We
// already announced this change by calling didPopNext.
return !(
nextRoute == null &&
lastAnnouncedPoppedNextRoute != null &&
lastAnnouncedPoppedNextRoute == lastAnnouncedNextRoute
);
}
static final _RouteEntryPredicate isPresentPredicate = (_RouteEntry entry) => entry.isPresent;
static final _RouteEntryPredicate willBePresentPredicate = (_RouteEntry entry) => entry.willBePresent;
static _RouteEntryPredicate isRoutePredicate(Route<dynamic> route) {
return (_RouteEntry entry) => entry.route == route;
}
}
/// The state for a [Navigator] widget.
class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final GlobalKey<OverlayState> _overlayKey = GlobalKey<OverlayState>();
final List<_RouteEntry> _history = <_RouteEntry>[];
/// The [FocusScopeNode] for the [FocusScope] that encloses the routes.
final FocusScopeNode focusScopeNode = FocusScopeNode(debugLabel: 'Navigator Scope');
bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends
@override
void initState() {
super.initState();
for (final NavigatorObserver observer in widget.observers) {
assert(observer.navigator == null);
observer._navigator = this;
}
// TODO(chunhtai): Uses pages after we add page api.
// https://github.com/flutter/flutter/issues/45938
_history.addAll(
widget.onGenerateInitialRoutes(this, widget.initialRoute ?? Navigator.defaultRouteName)
.map((Route<dynamic> route) => _RouteEntry(
route,
initialState: _RouteLifecycle.add,
),
),
);
assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }());
_flushHistoryUpdates();
assert(() { _debugLocked = false; return true; }());
} }
@override @override
...@@ -1579,8 +1980,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1579,8 +1980,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
observer._navigator = this; observer._navigator = this;
} }
} }
for (final Route<dynamic> route in _history) for (final _RouteEntry entry in _history)
route.changedExternalState(); entry.route.changedExternalState();
} }
@override @override
...@@ -1592,35 +1993,199 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1592,35 +1993,199 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
}()); }());
for (final NavigatorObserver observer in widget.observers) for (final NavigatorObserver observer in widget.observers)
observer._navigator = null; observer._navigator = null;
final List<Route<dynamic>> doomed = _poppedRoutes.toList()..addAll(_history);
for (final Route<dynamic> route in doomed)
route.dispose();
_poppedRoutes.clear();
_history.clear();
focusScopeNode.dispose(); focusScopeNode.dispose();
for (final _RouteEntry entry in _history)
entry.dispose();
super.dispose(); super.dispose();
assert(() { // don't unlock, so that the object becomes unusable
_debugLocked = false; assert(_debugLocked);
return true;
}());
} }
/// The overlay this navigator uses for its visual presentation. /// The overlay this navigator uses for its visual presentation.
OverlayState get overlay => _overlayKey.currentState; OverlayState get overlay => _overlayKey.currentState;
OverlayEntry get _currentOverlayEntry { Iterable<OverlayEntry> get _allRouteOverlayEntries sync* {
for (final Route<dynamic> route in _history.reversed) { for (final _RouteEntry entry in _history)
if (route.overlayEntries.isNotEmpty) yield* entry.route.overlayEntries;
return route.overlayEntries.last; }
void _flushHistoryUpdates({bool rearrangeOverlay = true}) {
assert(_debugLocked);
// Clean up the list, sending updates to the routes that changed. Notably,
// we don't send the didChangePrevious/didChangeNext updates to those that
// did not change at this point, because we're not yet sure exactly what the
// routes will be at the end of the day (some might get disposed).
int index = _history.length - 1;
_RouteEntry next;
_RouteEntry entry = _history[index];
_RouteEntry previous = index > 0 ? _history[index - 1] : null;
bool canRemove = false;
Route<dynamic> poppedRoute; // The route that should trigger didPopNext on the top active route.
bool seenTopActiveRoute = false; // Whether we've seen the route that would get didPopNext.
final List<_RouteEntry> toBeDisposed = <_RouteEntry>[];
while (index >= 0) {
switch (entry.currentState) {
case _RouteLifecycle.add:
assert(rearrangeOverlay);
entry.handleAdd(
navigator: this,
previous: previous?.route,
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
isNewFirst: next == null,
);
assert(entry.currentState == _RouteLifecycle.idle);
continue;
case _RouteLifecycle.push:
case _RouteLifecycle.pushReplace:
case _RouteLifecycle.replace:
assert(rearrangeOverlay);
entry.handlePush(
navigator: this,
previous: previous?.route,
previousPresent: _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate)?.route,
isNewFirst: next == null,
);
assert(entry.currentState != _RouteLifecycle.push);
assert(entry.currentState != _RouteLifecycle.pushReplace);
assert(entry.currentState != _RouteLifecycle.replace);
if (entry.currentState == _RouteLifecycle.idle) {
continue;
}
break;
case _RouteLifecycle.pushing: // Will exit this state when animation completes.
if (!seenTopActiveRoute && poppedRoute != null)
entry.handleDidPopNext(poppedRoute);
seenTopActiveRoute = true;
break;
case _RouteLifecycle.idle:
if (!seenTopActiveRoute && poppedRoute != null)
entry.handleDidPopNext(poppedRoute);
seenTopActiveRoute = true;
// This route is idle, so we are allowed to remove subsequent (earlier)
// routes that are waiting to be removed silently:
canRemove = true;
break;
case _RouteLifecycle.pop:
if (!seenTopActiveRoute) {
if (poppedRoute != null)
entry.handleDidPopNext(poppedRoute);
poppedRoute = entry.route;
}
entry.handlePop(
navigator: this,
previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
);
assert(entry.currentState == _RouteLifecycle.popping);
break;
case _RouteLifecycle.popping:
// Will exit this state when animation completes.
break;
case _RouteLifecycle.remove:
if (!seenTopActiveRoute) {
if (poppedRoute != null)
entry.route.didPopNext(poppedRoute);
poppedRoute = null;
}
entry.handleRemoval(
navigator: this,
previousPresent: _getRouteBefore(index, _RouteEntry.willBePresentPredicate)?.route,
);
assert(entry.currentState == _RouteLifecycle.removing);
continue;
case _RouteLifecycle.removing:
if (!canRemove && next != null) {
// We aren't allowed to remove this route yet.
break;
}
entry.currentState = _RouteLifecycle.dispose;
continue;
case _RouteLifecycle.dispose:
// Delay disposal until didChangeNext/didChangePrevious have been sent.
toBeDisposed.add(_history.removeAt(index));
entry = next;
break;
case _RouteLifecycle.disposed:
assert(false);
break;
}
index -= 1;
next = entry;
entry = previous;
previous = index > 0 ? _history[index - 1] : null;
} }
return null; // Now that the list is clean, send the didChangeNext/didChangePrevious
// notifications.
_flushRouteAnnouncement();
// Lastly, removes the overlay entries of all marked entries and disposes
// them.
for (final _RouteEntry entry in toBeDisposed) {
for (final OverlayEntry overlayEntry in entry.route.overlayEntries)
overlayEntry.remove();
entry.dispose();
}
if (rearrangeOverlay)
overlay?.rearrange(_allRouteOverlayEntries);
} }
bool _debugLocked = false; // used to prevent re-entrant calls to push, pop, and friends void _flushRouteAnnouncement() {
int index = _history.length - 1;
while (index >= 0) {
final _RouteEntry entry = _history[index];
final _RouteEntry next = _getRouteAfter(index + 1, _RouteEntry.isPresentPredicate);
if (next?.route != entry.lastAnnouncedNextRoute) {
if (entry.shouldAnnounceChangeToNext(next?.route)) {
entry.route.didChangeNext(next?.route);
}
entry.lastAnnouncedNextRoute = next?.route;
}
final _RouteEntry previous = _getRouteBefore(index - 1, _RouteEntry.isPresentPredicate);
if (previous?.route != entry.lastAnnouncedPreviousRoute) {
entry.route.didChangePrevious(previous?.route);
entry.lastAnnouncedPreviousRoute = previous?.route;
}
index -= 1;
}
}
_RouteEntry _getRouteBefore(int index, _RouteEntryPredicate predicate) {
index = _getIndexBefore(index, predicate);
return index >= 0 ? _history[index] : null;
}
int _getIndexBefore(int index, _RouteEntryPredicate predicate) {
while(index >= 0 && !predicate(_history[index])) {
index -= 1;
}
return index;
}
_RouteEntry _getRouteAfter(int index, _RouteEntryPredicate predicate) {
while (index < _history.length && !predicate(_history[index])) {
index += 1;
}
return index < _history.length ? _history[index] : null;
}
Route<T> _routeNamed<T>(String name, { @required Object arguments, bool allowNull = false }) { Route<T> _routeNamed<T>(String name, { @required Object arguments, bool allowNull = false }) {
assert(!_debugLocked); assert(!_debugLocked);
assert(name != null); assert(name != null);
if (allowNull && widget.onGenerateRoute == null)
return null;
assert(() {
if (widget.onGenerateRoute == null) {
throw FlutterError(
'Navigator.onGenerateRoute was null, but the route named "$name" was referenced.\n'
'To use the Navigator API with named routes (pushNamed, pushReplacementNamed, or '
'pushNamedAndRemoveUntil), the Navigator must be provided with an '
'onGenerateRoute handler.\n'
'The Navigator was:\n'
' $this'
);
}
return true;
}());
final RouteSettings settings = RouteSettings( final RouteSettings settings = RouteSettings(
name: name, name: name,
isInitialRoute: _history.isEmpty, isInitialRoute: _history.isEmpty,
...@@ -1631,10 +2196,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1631,10 +2196,10 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
assert(() { assert(() {
if (widget.onUnknownRoute == null) { if (widget.onUnknownRoute == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('If a Navigator has no onUnknownRoute, then its onGenerateRoute must never return null.'), ErrorSummary('Navigator.onGenerateRoute returned null when requested to build route "$name".'),
ErrorDescription( ErrorDescription(
'When trying to build the route "$name", onGenerateRoute returned null, but there was no ' 'The onGenerateRoute callback must never return null, unless an onUnknownRoute '
'onUnknownRoute callback specified.' 'callback is provided as well.'
), ),
DiagnosticsProperty<NavigatorState>('The Navigator was', this, style: DiagnosticsTreeStyle.errorProperty), DiagnosticsProperty<NavigatorState>('The Navigator was', this, style: DiagnosticsTreeStyle.errorProperty),
]); ]);
...@@ -1645,17 +2210,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1645,17 +2210,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
assert(() { assert(() {
if (route == null) { if (route == null) {
throw FlutterError.fromParts(<DiagnosticsNode>[ throw FlutterError.fromParts(<DiagnosticsNode>[
ErrorSummary('A Navigator\'s onUnknownRoute returned null.'), ErrorSummary('Navigator.onUnknownRoute returned null when requested to build route "$name".'),
ErrorDescription( ErrorDescription('The onUnknownRoute callback must never return null.'),
'When trying to build the route "$name", both onGenerateRoute and onUnknownRoute returned '
'null. The onUnknownRoute callback should never return null.'
),
DiagnosticsProperty<NavigatorState>('The Navigator was', this, style: DiagnosticsTreeStyle.errorProperty), DiagnosticsProperty<NavigatorState>('The Navigator was', this, style: DiagnosticsTreeStyle.errorProperty),
]); ]);
} }
return true; return true;
}()); }());
} }
assert(route != null || allowNull);
return route; return route;
} }
...@@ -1786,19 +2349,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1786,19 +2349,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
}()); }());
assert(route != null); assert(route != null);
assert(route._navigator == null); assert(route._navigator == null);
final Route<dynamic> oldRoute = _history.isNotEmpty ? _history.last : null; _history.add(_RouteEntry(route, initialState: _RouteLifecycle.push));
route._navigator = this; _flushHistoryUpdates();
route.install(_currentOverlayEntry);
_history.add(route);
route.didPush();
route.didChangeNext(null);
if (oldRoute != null) {
oldRoute.didChangeNext(route);
route.didChangePrevious(oldRoute);
}
for (final NavigatorObserver observer in widget.observers)
observer.didPush(route, oldRoute);
RouteNotificationMessages.maybeNotifyRouteChange(_routePushedMethod, route, oldRoute);
assert(() { assert(() {
_debugLocked = false; _debugLocked = false;
return true; return true;
...@@ -1829,7 +2381,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1829,7 +2381,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final RouteSettings settings = route.settings; final RouteSettings settings = route.settings;
final Map<String, dynamic> settingsJsonable = <String, dynamic> { final Map<String, dynamic> settingsJsonable = <String, dynamic> {
'name': settings.name, 'name': settings.name,
'isInitialRoute': settings.isInitialRoute,
}; };
if (settings.arguments != null) { if (settings.arguments != null) {
settingsJsonable['arguments'] = jsonEncode( settingsJsonable['arguments'] = jsonEncode(
...@@ -1871,35 +2422,13 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1871,35 +2422,13 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_debugLocked = true; _debugLocked = true;
return true; return true;
}()); }());
final Route<dynamic> oldRoute = _history.last; assert(newRoute != null);
assert(oldRoute != null && oldRoute._navigator == this);
assert(oldRoute.overlayEntries.isNotEmpty);
assert(newRoute._navigator == null); assert(newRoute._navigator == null);
assert(newRoute.overlayEntries.isEmpty); assert(_history.isNotEmpty);
final int index = _history.length - 1; assert(_history.any(_RouteEntry.isPresentPredicate), 'Navigator has no active routes to replace.');
assert(index >= 0); _history.lastWhere(_RouteEntry.isPresentPredicate).complete(result, isReplaced: true);
assert(_history.indexOf(oldRoute) == index); _history.add(_RouteEntry(newRoute, initialState: _RouteLifecycle.pushReplace));
newRoute._navigator = this; _flushHistoryUpdates();
newRoute.install(_currentOverlayEntry);
_history[index] = newRoute;
newRoute.didPush().whenCompleteOrCancel(() {
// The old route's exit is not animated. We're assuming that the
// new route completely obscures the old one.
if (mounted) {
oldRoute
..didComplete(result ?? oldRoute.currentResult)
..dispose();
}
});
newRoute.didChangeNext(null);
oldRoute.didChangeNext(newRoute);
if (index > 0) {
_history[index - 1].didChangeNext(newRoute);
newRoute.didChangePrevious(_history[index - 1]);
}
for (final NavigatorObserver observer in widget.observers)
observer.didReplace(newRoute: newRoute, oldRoute: oldRoute);
RouteNotificationMessages.maybeNotifyRouteChange(_routeReplacedMethod, newRoute, oldRoute);
assert(() { assert(() {
_debugLocked = false; _debugLocked = false;
return true; return true;
...@@ -1933,49 +2462,19 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -1933,49 +2462,19 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_debugLocked = true; _debugLocked = true;
return true; return true;
}()); }());
assert(newRoute != null);
// The route that is being pushed on top of
final Route<dynamic> precedingRoute = _history.isNotEmpty ? _history.last : null;
final OverlayEntry precedingRouteOverlay = _currentOverlayEntry;
// Routes to remove
final List<Route<dynamic>> removedRoutes = <Route<dynamic>>[];
while (_history.isNotEmpty && !predicate(_history.last)) {
final Route<dynamic> removedRoute = _history.removeLast();
assert(removedRoute != null && removedRoute._navigator == this);
assert(removedRoute.overlayEntries.isNotEmpty);
removedRoutes.add(removedRoute);
}
// Push new route
assert(newRoute._navigator == null); assert(newRoute._navigator == null);
assert(newRoute.overlayEntries.isEmpty); assert(newRoute.overlayEntries.isEmpty);
final Route<dynamic> newPrecedingRoute = _history.isNotEmpty ? _history.last : null; assert(predicate != null);
newRoute._navigator = this; int index = _history.length - 1;
newRoute.install(precedingRouteOverlay); _history.add(_RouteEntry(newRoute, initialState: _RouteLifecycle.push));
_history.add(newRoute); while (index >= 0) {
final _RouteEntry entry = _history[index];
newRoute.didPush().whenCompleteOrCancel(() { if (entry.isPresent && !predicate(entry.route))
if (mounted) { _history[index].remove();
for (final Route<dynamic> removedRoute in removedRoutes) { index -= 1;
for (final NavigatorObserver observer in widget.observers)
observer.didRemove(removedRoute, newPrecedingRoute);
removedRoute.dispose();
}
}
});
// Notify for newRoute
newRoute.didChangeNext(null);
if (precedingRoute != null) {
precedingRoute.didChangeNext(newRoute);
}
if (newPrecedingRoute != null) {
newPrecedingRoute.didChangeNext(newRoute);
newRoute.didChangePrevious(newPrecedingRoute);
} }
for (final NavigatorObserver observer in widget.observers) _flushHistoryUpdates();
observer.didPush(newRoute, precedingRoute);
assert(() { assert(() {
_debugLocked = false; _debugLocked = false;
...@@ -2006,33 +2505,19 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2006,33 +2505,19 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
}()); }());
assert(oldRoute._navigator == this); assert(oldRoute._navigator == this);
assert(newRoute._navigator == null); assert(newRoute._navigator == null);
assert(oldRoute.overlayEntries.isNotEmpty); final int index = _history.indexWhere(_RouteEntry.isRoutePredicate(oldRoute));
assert(newRoute.overlayEntries.isEmpty); assert(index >= 0, 'This Navigator does not contain the specified oldRoute.');
assert(!overlay.debugIsVisible(oldRoute.overlayEntries.last)); assert(_history[index].isPresent, 'The specified oldRoute has already been removed from the Navigator.');
final int index = _history.indexOf(oldRoute); final bool wasCurrent = oldRoute.isCurrent;
assert(index >= 0); _history.insert(index + 1, _RouteEntry(newRoute, initialState: _RouteLifecycle.replace));
newRoute._navigator = this; _history[index].remove(isReplaced: true);
newRoute.install(oldRoute.overlayEntries.last); _flushHistoryUpdates();
_history[index] = newRoute;
newRoute.didReplace(oldRoute);
if (index + 1 < _history.length) {
newRoute.didChangeNext(_history[index + 1]);
_history[index + 1].didChangePrevious(newRoute);
} else {
newRoute.didChangeNext(null);
}
if (index > 0) {
_history[index - 1].didChangeNext(newRoute);
newRoute.didChangePrevious(_history[index - 1]);
}
for (final NavigatorObserver observer in widget.observers)
observer.didReplace(newRoute: newRoute, oldRoute: oldRoute);
RouteNotificationMessages.maybeNotifyRouteChange(_routeReplacedMethod, newRoute, oldRoute);
oldRoute.dispose();
assert(() { assert(() {
_debugLocked = false; _debugLocked = false;
return true; return true;
}()); }());
if (wasCurrent)
_afterNavigation(newRoute);
} }
/// Replaces a route on the navigator with a new route. The route to be /// Replaces a route on the navigator with a new route. The route to be
...@@ -2045,11 +2530,27 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2045,11 +2530,27 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// * [replace], which is the same but identifies the route to be removed /// * [replace], which is the same but identifies the route to be removed
/// directly. /// directly.
@optionalTypeArgs @optionalTypeArgs
void replaceRouteBelow<T extends Object>({ @required Route<dynamic> anchorRoute, Route<T> newRoute }) { void replaceRouteBelow<T extends Object>({ @required Route<dynamic> anchorRoute, @required Route<T> newRoute }) {
assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }());
assert(anchorRoute != null); assert(anchorRoute != null);
assert(anchorRoute._navigator == this); assert(anchorRoute._navigator == this);
assert(_history.indexOf(anchorRoute) > 0); assert(newRoute != null);
replace<T>(oldRoute: _history[_history.indexOf(anchorRoute) - 1], newRoute: newRoute); assert(newRoute._navigator == null);
final int anchorIndex = _history.indexWhere(_RouteEntry.isRoutePredicate(anchorRoute));
assert(anchorIndex >= 0, 'This Navigator does not contain the specified anchorRoute.');
assert(_history[anchorIndex].isPresent, 'The specified anchorRoute has already been removed from the Navigator.');
int index = anchorIndex - 1;
while (index >= 0) {
if (_history[index].isPresent)
break;
index -= 1;
}
assert(index >= 0, 'There are no routes below the specified anchorRoute.');
_history.insert(index + 1, _RouteEntry(newRoute, initialState: _RouteLifecycle.replace));
_history[index].remove(isReplaced: true);
_flushHistoryUpdates();
assert(() { _debugLocked = false; return true; }());
} }
/// Whether the navigator can be popped. /// Whether the navigator can be popped.
...@@ -2061,12 +2562,19 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2061,12 +2562,19 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// * [Route.isFirst], which returns true for routes for which [canPop] /// * [Route.isFirst], which returns true for routes for which [canPop]
/// returns false. /// returns false.
bool canPop() { bool canPop() {
assert(_history.isNotEmpty); final Iterator<_RouteEntry> iterator = _history.where(_RouteEntry.isPresentPredicate).iterator;
return _history.length > 1 || _history[0].willHandlePopInternally; if (!iterator.moveNext())
return false; // we have no active routes, so we can't pop
if (iterator.current.route.willHandlePopInternally)
return true; // the first route can handle pops itself, so we can pop
if (!iterator.moveNext())
return false; // there's only one route, so we can't pop
return true; // there's at least two routes, so we can pop
} }
/// Tries to pop the current route, while honoring the route's [Route.willPop] /// Consults the current route's [Route.willPop] method, and acts accordingly,
/// state. /// potentially popping the route as a result; returns whether the pop request
/// should be considered handled.
/// ///
/// {@macro flutter.widgets.navigator.maybePop} /// {@macro flutter.widgets.navigator.maybePop}
/// ///
...@@ -2076,17 +2584,28 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2076,17 +2584,28 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// to veto a [pop] initiated by the app's back button. /// to veto a [pop] initiated by the app's back button.
/// * [ModalRoute], which provides a `scopedWillPopCallback` that can be used /// * [ModalRoute], which provides a `scopedWillPopCallback` that can be used
/// to define the route's `willPop` method. /// to define the route's `willPop` method.
@optionalTypeArgs
Future<bool> maybePop<T extends Object>([ T result ]) async { Future<bool> maybePop<T extends Object>([ T result ]) async {
final Route<T> route = _history.last as Route<T>; final _RouteEntry lastEntry = _history.lastWhere(_RouteEntry.isPresentPredicate, orElse: () => null);
assert(route._navigator == this); if (lastEntry == null)
final RoutePopDisposition disposition = await route.willPop(); return false;
if (disposition != RoutePopDisposition.bubble && mounted) { assert(lastEntry.route._navigator == this);
if (disposition == RoutePopDisposition.pop) final RoutePopDisposition disposition = await lastEntry.route.willPop(); // this is asynchronous
assert(disposition != null);
if (!mounted)
return true; // forget about this pop, we were disposed in the meantime
final _RouteEntry newLastEntry = _history.lastWhere(_RouteEntry.isPresentPredicate, orElse: () => null);
if (lastEntry != newLastEntry)
return true; // forget about this pop, something happened to our history in the meantime
switch (disposition) {
case RoutePopDisposition.bubble:
return false;
case RoutePopDisposition.pop:
pop(result); pop(result);
return true; return true;
case RoutePopDisposition.doNotPop:
return true;
} }
return false; return null;
} }
/// Pop the top-most route off the navigator. /// Pop the top-most route off the navigator.
...@@ -2114,48 +2633,25 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2114,48 +2633,25 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
@optionalTypeArgs @optionalTypeArgs
bool pop<T extends Object>([ T result ]) { void pop<T extends Object>([ T result ]) {
assert(!_debugLocked); assert(!_debugLocked);
assert(() { assert(() {
_debugLocked = true; _debugLocked = true;
return true; return true;
}()); }());
final Route<dynamic> route = _history.last; final _RouteEntry entry = _history.lastWhere(_RouteEntry.isPresentPredicate);
assert(route._navigator == this); entry.pop<T>(result);
bool debugPredictedWouldPop; if (entry.currentState == _RouteLifecycle.pop) {
assert(() { // Flush the history if the route actually wants to be popped (the pop
debugPredictedWouldPop = !route.willHandlePopInternally; // wasn't handled internally).
return true; _flushHistoryUpdates(rearrangeOverlay: false);
}()); assert(entry.route._popCompleter.isCompleted);
if (route.didPop(result ?? route.currentResult)) {
assert(debugPredictedWouldPop);
if (_history.length > 1) {
_history.removeLast();
// If route._navigator is null, the route called finalizeRoute from
// didPop, which means the route has already been disposed and doesn't
// need to be added to _poppedRoutes for later disposal.
if (route._navigator != null)
_poppedRoutes.add(route);
_history.last.didPopNext(route);
for (final NavigatorObserver observer in widget.observers)
observer.didPop(route, _history.last);
RouteNotificationMessages.maybeNotifyRouteChange(_routePoppedMethod, route, _history.last);
} else {
assert(() {
_debugLocked = false;
return true;
}());
return false;
}
} else {
assert(!debugPredictedWouldPop);
} }
assert(() { assert(() {
_debugLocked = false; _debugLocked = false;
return true; return true;
}()); }());
_afterNavigation<dynamic>(route); _afterNavigation<dynamic>(entry.route);
return true;
} }
/// Calls [pop] repeatedly until the predicate returns true. /// Calls [pop] repeatedly until the predicate returns true.
...@@ -2173,8 +2669,9 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2173,8 +2669,9 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
void popUntil(RoutePredicate predicate) { void popUntil(RoutePredicate predicate) {
while (!predicate(_history.last)) while (!predicate(_history.lastWhere(_RouteEntry.isPresentPredicate).route)) {
pop(); pop();
}
} }
/// Immediately remove `route` from the navigator, and [Route.dispose] it. /// Immediately remove `route` from the navigator, and [Route.dispose] it.
...@@ -2188,21 +2685,22 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2188,21 +2685,22 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return true; return true;
}()); }());
assert(route._navigator == this); assert(route._navigator == this);
final int index = _history.indexOf(route); final bool wasCurrent = route.isCurrent;
assert(index != -1); final _RouteEntry entry = _history.firstWhere(_RouteEntry.isRoutePredicate(route), orElse: () => null);
final Route<dynamic> previousRoute = index > 0 ? _history[index - 1] : null; assert(entry != null);
final Route<dynamic> nextRoute = (index + 1 < _history.length) ? _history[index + 1] : null; entry.remove();
_history.removeAt(index); _flushHistoryUpdates(rearrangeOverlay: false);
previousRoute?.didChangeNext(nextRoute);
nextRoute?.didChangePrevious(previousRoute);
for (final NavigatorObserver observer in widget.observers)
observer.didRemove(route, previousRoute);
route.dispose();
assert(() { assert(() {
_debugLocked = false; _debugLocked = false;
return true; return true;
}()); }());
_afterNavigation<dynamic>(nextRoute); if (wasCurrent)
_afterNavigation<dynamic>(
_history.lastWhere(
_RouteEntry.isPresentPredicate,
orElse: () => null
)?.route
);
} }
/// Immediately remove a route from the navigator, and [Route.dispose] it. The /// Immediately remove a route from the navigator, and [Route.dispose] it. The
...@@ -2215,20 +2713,20 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2215,20 +2713,20 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_debugLocked = true; _debugLocked = true;
return true; return true;
}()); }());
assert(anchorRoute != null);
assert(anchorRoute._navigator == this); assert(anchorRoute._navigator == this);
final int index = _history.indexOf(anchorRoute) - 1; final int anchorIndex = _history.indexWhere(_RouteEntry.isRoutePredicate(anchorRoute));
assert(index >= 0); assert(anchorIndex >= 0, 'This Navigator does not contain the specified anchorRoute.');
final Route<dynamic> targetRoute = _history[index]; assert(_history[anchorIndex].isPresent, 'The specified anchorRoute has already been removed from the Navigator.');
assert(targetRoute._navigator == this); int index = anchorIndex - 1;
assert(targetRoute.overlayEntries.isEmpty || !overlay.debugIsVisible(targetRoute.overlayEntries.last)); while (index >= 0) {
_history.removeAt(index); if (_history[index].isPresent)
final Route<dynamic> nextRoute = index < _history.length ? _history[index] : null; break;
final Route<dynamic> previousRoute = index > 0 ? _history[index - 1] : null; index -= 1;
if (previousRoute != null) }
previousRoute.didChangeNext(nextRoute); assert(index >= 0, 'There are no routes below the specified anchorRoute.');
if (nextRoute != null) _history[index].remove();
nextRoute.didChangePrevious(previousRoute); _flushHistoryUpdates(rearrangeOverlay: false);
targetRoute.dispose();
assert(() { assert(() {
_debugLocked = false; _debugLocked = false;
return true; return true;
...@@ -2247,8 +2745,23 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2247,8 +2745,23 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// This function may be called directly from [Route.didPop] if [Route.didPop] /// This function may be called directly from [Route.didPop] if [Route.didPop]
/// will return true. /// will return true.
void finalizeRoute(Route<dynamic> route) { void finalizeRoute(Route<dynamic> route) {
_poppedRoutes.remove(route); // FinalizeRoute may have been called while we were already locked as a
route.dispose(); // responds to route.didPop(). Make sure to leave in the state we were in
// before the call.
bool wasDebugLocked;
assert(() { wasDebugLocked = _debugLocked; _debugLocked = true; return true; }());
assert(_history.where(_RouteEntry.isRoutePredicate(route)).length == 1);
final _RouteEntry entry = _history.firstWhere(_RouteEntry.isRoutePredicate(route));
if (entry.doingPop) {
// We were called synchronously from Route.didPop(), but didn't process
// the pop yet. Let's do that now before finalizing.
entry.currentState = _RouteLifecycle.pop;
_flushHistoryUpdates(rearrangeOverlay: false);
}
assert(entry.currentState != _RouteLifecycle.pop);
entry.finalize();
_flushHistoryUpdates(rearrangeOverlay: false);
assert(() { _debugLocked = wasDebugLocked; return true; }());
} }
int get _userGesturesInProgress => _userGesturesInProgressCount; int get _userGesturesInProgress => _userGesturesInProgressCount;
...@@ -2279,13 +2792,19 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2279,13 +2792,19 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
void didStartUserGesture() { void didStartUserGesture() {
_userGesturesInProgress += 1; _userGesturesInProgress += 1;
if (_userGesturesInProgress == 1) { if (_userGesturesInProgress == 1) {
final Route<dynamic> route = _history.last; final int routeIndex = _getIndexBefore(
final Route<dynamic> previousRoute = !route.willHandlePopInternally && _history.length > 1 _history.length - 1,
? _history[_history.length - 2] _RouteEntry.willBePresentPredicate,
: null; );
// Don't operate the _history list since the gesture may be canceled. assert(routeIndex != null);
// In case of a back swipe, the gesture controller will call .pop() itself. final Route<dynamic> route = _history[routeIndex].route;
Route<dynamic> previousRoute;
if (!route.willHandlePopInternally && routeIndex > 0) {
previousRoute = _getRouteBefore(
routeIndex - 1,
_RouteEntry.willBePresentPredicate,
).route;
}
for (final NavigatorObserver observer in widget.observers) for (final NavigatorObserver observer in widget.observers)
observer.didStartUserGesture(route, previousRoute); observer.didStartUserGesture(route, previousRoute);
} }
...@@ -2345,7 +2864,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin { ...@@ -2345,7 +2864,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
autofocus: true, autofocus: true,
child: Overlay( child: Overlay(
key: _overlayKey, key: _overlayKey,
initialEntries: _initialOverlayEntries, initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
), ),
), ),
), ),
......
...@@ -34,14 +34,6 @@ abstract class PageRoute<T> extends ModalRoute<T> { ...@@ -34,14 +34,6 @@ abstract class PageRoute<T> extends ModalRoute<T> {
@override @override
bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => previousRoute is PageRoute; bool canTransitionFrom(TransitionRoute<dynamic> previousRoute) => previousRoute is PageRoute;
@override
AnimationController createAnimationController() {
final AnimationController controller = super.createAnimationController();
if (settings.isInitialRoute)
controller.value = 1.0;
return controller;
}
} }
Widget _defaultTransitionsBuilder(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) { Widget _defaultTransitionsBuilder(BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation, Widget child) {
......
...@@ -32,17 +32,15 @@ abstract class OverlayRoute<T> extends Route<T> { ...@@ -32,17 +32,15 @@ abstract class OverlayRoute<T> extends Route<T> {
/// Subclasses should override this getter to return the builders for the overlay. /// Subclasses should override this getter to return the builders for the overlay.
Iterable<OverlayEntry> createOverlayEntries(); Iterable<OverlayEntry> createOverlayEntries();
/// The entries this route has placed in the overlay.
@override @override
List<OverlayEntry> get overlayEntries => _overlayEntries; List<OverlayEntry> get overlayEntries => _overlayEntries;
final List<OverlayEntry> _overlayEntries = <OverlayEntry>[]; final List<OverlayEntry> _overlayEntries = <OverlayEntry>[];
@override @override
void install(OverlayEntry insertionPoint) { void install() {
assert(_overlayEntries.isEmpty); assert(_overlayEntries.isEmpty);
_overlayEntries.addAll(createOverlayEntries()); _overlayEntries.addAll(createOverlayEntries());
navigator.overlay?.insertAll(_overlayEntries, above: insertionPoint); super.install();
super.install(insertionPoint);
} }
/// Controls whether [didPop] calls [NavigatorState.finalizeRoute]. /// Controls whether [didPop] calls [NavigatorState.finalizeRoute].
...@@ -68,8 +66,6 @@ abstract class OverlayRoute<T> extends Route<T> { ...@@ -68,8 +66,6 @@ abstract class OverlayRoute<T> extends Route<T> {
@override @override
void dispose() { void dispose() {
for (final OverlayEntry entry in _overlayEntries)
entry.remove();
_overlayEntries.clear(); _overlayEntries.clear();
super.dispose(); super.dispose();
} }
...@@ -111,6 +107,10 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -111,6 +107,10 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
/// the opaque route will not be built to save resources. /// the opaque route will not be built to save resources.
bool get opaque; bool get opaque;
// This ensures that if we got to the dismissed state while still current,
// we will still be disposed when we are eventually popped.
//
// This situation arises when dealing with the Cupertino dismiss gesture.
@override @override
bool get finishedWhenPopped => _controller.status == AnimationStatus.dismissed; bool get finishedWhenPopped => _controller.status == AnimationStatus.dismissed;
...@@ -185,13 +185,13 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -185,13 +185,13 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
} }
@override @override
void install(OverlayEntry insertionPoint) { void install() {
assert(!_transitionCompleter.isCompleted, 'Cannot install a $runtimeType after disposing it.'); assert(!_transitionCompleter.isCompleted, 'Cannot install a $runtimeType after disposing it.');
_controller = createAnimationController(); _controller = createAnimationController();
assert(_controller != null, '$runtimeType.createAnimationController() returned null.'); assert(_controller != null, '$runtimeType.createAnimationController() returned null.');
_animation = createAnimation(); _animation = createAnimation();
assert(_animation != null, '$runtimeType.createAnimation() returned null.'); assert(_animation != null, '$runtimeType.createAnimation() returned null.');
super.install(insertionPoint); super.install();
} }
@override @override
...@@ -203,6 +203,15 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> { ...@@ -203,6 +203,15 @@ abstract class TransitionRoute<T> extends OverlayRoute<T> {
return _controller.forward(); return _controller.forward();
} }
@override
void didAdd() {
assert(_controller != null, '$runtimeType.didPush called before calling install() or after calling dispose().');
assert(!_transitionCompleter.isCompleted, 'Cannot reuse a $runtimeType after disposing it.');
_didPushOrReplace();
super.didAdd();
_controller.value = _controller.upperBound;
}
@override @override
void didReplace(Route<dynamic> oldRoute) { void didReplace(Route<dynamic> oldRoute) {
assert(_controller != null, '$runtimeType.didReplace called before calling install() or after calling dispose().'); assert(_controller != null, '$runtimeType.didReplace called before calling install() or after calling dispose().');
...@@ -548,7 +557,7 @@ mixin LocalHistoryRoute<T> on Route<T> { ...@@ -548,7 +557,7 @@ mixin LocalHistoryRoute<T> on Route<T> {
Future<RoutePopDisposition> willPop() async { Future<RoutePopDisposition> willPop() async {
if (willHandlePopInternally) if (willHandlePopInternally)
return RoutePopDisposition.pop; return RoutePopDisposition.pop;
return await super.willPop(); return super.willPop();
} }
@override @override
...@@ -968,8 +977,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -968,8 +977,8 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
} }
@override @override
void install(OverlayEntry insertionPoint) { void install() {
super.install(insertionPoint); super.install();
_animationProxy = ProxyAnimation(super.animation); _animationProxy = ProxyAnimation(super.animation);
_secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation); _secondaryAnimationProxy = ProxyAnimation(super.secondaryAnimation);
} }
...@@ -982,6 +991,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T ...@@ -982,6 +991,14 @@ abstract class ModalRoute<T> extends TransitionRoute<T> with LocalHistoryRoute<T
return super.didPush(); return super.didPush();
} }
@override
void didAdd() {
if (_scopeKey.currentState != null) {
navigator.focusScopeNode.setFirstFocus(_scopeKey.currentState.focusScopeNode);
}
super.didAdd();
}
// The API for subclasses to override - used by this class // The API for subclasses to override - used by this class
/// Whether you can dismiss this route by tapping the modal barrier. /// Whether you can dismiss this route by tapping the modal barrier.
......
...@@ -84,4 +84,49 @@ void main() { ...@@ -84,4 +84,49 @@ void main() {
expect(tester.widget<Title>(find.byType(Title)).color.value, 0xFF000001); expect(tester.widget<Title>(find.byType(Title)).color.value, 0xFF000001);
}); });
testWidgets('Can customize initial routes', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(
CupertinoApp(
navigatorKey: navigatorKey,
onGenerateInitialRoutes: (String initialRoute) {
expect(initialRoute, '/abc');
return <Route<void>>[
PageRouteBuilder<void>(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation) {
return const Text('non-regular page one');
}
),
PageRouteBuilder<void>(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation) {
return const Text('non-regular page two');
}
),
];
},
initialRoute: '/abc',
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => const Text('regular page one'),
'/abc': (BuildContext context) => const Text('regular page two'),
},
)
);
expect(find.text('non-regular page two'), findsOneWidget);
expect(find.text('non-regular page one'), findsNothing);
expect(find.text('regular page one'), findsNothing);
expect(find.text('regular page two'), findsNothing);
navigatorKey.currentState.pop();
await tester.pumpAndSettle();
expect(find.text('non-regular page two'), findsNothing);
expect(find.text('non-regular page one'), findsOneWidget);
expect(find.text('regular page one'), findsNothing);
expect(find.text('regular page two'), findsNothing);
});
} }
...@@ -788,6 +788,51 @@ void main() { ...@@ -788,6 +788,51 @@ void main() {
expect(themeBeforeBrightnessChange.brightness, Brightness.light); expect(themeBeforeBrightnessChange.brightness, Brightness.light);
expect(themeAfterBrightnessChange.brightness, Brightness.dark); expect(themeAfterBrightnessChange.brightness, Brightness.dark);
}); });
testWidgets('MaterialApp can customize initial routes', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(
MaterialApp(
navigatorKey: navigatorKey,
onGenerateInitialRoutes: (String initialRoute) {
expect(initialRoute, '/abc');
return <Route<void>>[
PageRouteBuilder<void>(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation) {
return const Text('non-regular page one');
}
),
PageRouteBuilder<void>(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation) {
return const Text('non-regular page two');
}
),
];
},
initialRoute: '/abc',
routes: <String, WidgetBuilder>{
'/': (BuildContext context) => const Text('regular page one'),
'/abc': (BuildContext context) => const Text('regular page two'),
},
)
);
expect(find.text('non-regular page two'), findsOneWidget);
expect(find.text('non-regular page one'), findsNothing);
expect(find.text('regular page one'), findsNothing);
expect(find.text('regular page two'), findsNothing);
navigatorKey.currentState.pop();
await tester.pumpAndSettle();
expect(find.text('non-regular page two'), findsNothing);
expect(find.text('non-regular page one'), findsOneWidget);
expect(find.text('regular page one'), findsNothing);
expect(find.text('regular page two'), findsNothing);
});
} }
class MockAccessibilityFeature extends Mock implements AccessibilityFeatures {} class MockAccessibilityFeature extends Mock implements AccessibilityFeatures {}
...@@ -163,4 +163,54 @@ void main() { ...@@ -163,4 +163,54 @@ void main() {
); );
}); });
}); });
testWidgets('WidgetsApp can customize initial routes', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget(
WidgetsApp(
navigatorKey: navigatorKey,
onGenerateInitialRoutes: (String initialRoute) {
expect(initialRoute, '/abc');
return <Route<void>>[
PageRouteBuilder<void>(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation) {
return const Text('non-regular page one');
}
),
PageRouteBuilder<void>(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation) {
return const Text('non-regular page two');
}
),
];
},
initialRoute: '/abc',
onGenerateRoute: (RouteSettings settings) {
return PageRouteBuilder<void>(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation) {
return const Text('regular page');
}
);
},
color: const Color(0xFF123456),
)
);
expect(find.text('non-regular page two'), findsOneWidget);
expect(find.text('non-regular page one'), findsNothing);
expect(find.text('regular page'), findsNothing);
navigatorKey.currentState.pop();
await tester.pumpAndSettle();
expect(find.text('non-regular page two'), findsNothing);
expect(find.text('non-regular page one'), findsOneWidget);
expect(find.text('regular page'), findsNothing);
});
} }
...@@ -2188,7 +2188,7 @@ Future<void> main() async { ...@@ -2188,7 +2188,7 @@ Future<void> main() async {
// The element should be mounted and unique. // The element should be mounted and unique.
expect(state1.mounted, isTrue); expect(state1.mounted, isTrue);
expect(navigatorKey.currentState.pop(), isTrue); navigatorKey.currentState.pop();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
// State is preserved. // State is preserved.
......
...@@ -197,7 +197,7 @@ void main() { ...@@ -197,7 +197,7 @@ void main() {
height: 300.0, height: 300.0,
child: Navigator( child: Navigator(
onGenerateRoute: (RouteSettings settings) { onGenerateRoute: (RouteSettings settings) {
if (settings.isInitialRoute) { if (settings.name == '/') {
return MaterialPageRoute<void>( return MaterialPageRoute<void>(
builder: (BuildContext context) { builder: (BuildContext context) {
return RaisedButton( return RaisedButton(
...@@ -453,7 +453,7 @@ void main() { ...@@ -453,7 +453,7 @@ void main() {
expect(isPopped, isFalse); expect(isPopped, isFalse);
}); });
testWidgets('replaceNamed', (WidgetTester tester) async { testWidgets('replaceNamed replaces', (WidgetTester tester) async {
final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{ final Map<String, WidgetBuilder> routes = <String, WidgetBuilder>{
'/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushReplacementNamed(context, '/A'); }), '/' : (BuildContext context) => OnTapPage(id: '/', onTap: () { Navigator.pushReplacementNamed(context, '/A'); }),
'/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pushReplacementNamed(context, '/B'); }), '/A': (BuildContext context) => OnTapPage(id: 'A', onTap: () { Navigator.pushReplacementNamed(context, '/B'); }),
...@@ -1332,10 +1332,10 @@ void main() { ...@@ -1332,10 +1332,10 @@ void main() {
error.toStringDeep(), error.toStringDeep(),
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'FlutterError\n' 'FlutterError\n'
' If a Navigator has no onUnknownRoute, then its onGenerateRoute\n' ' Navigator.onGenerateRoute returned null when requested to build\n'
' must never return null.\n' ' route "/".\n'
' When trying to build the route "/", onGenerateRoute returned\n' ' The onGenerateRoute callback must never return null, unless an\n'
' null, but there was no onUnknownRoute callback specified.\n' ' onUnknownRoute callback is provided as well.\n'
' The Navigator was:\n' ' The Navigator was:\n'
' NavigatorState#4d6bf(lifecycle state: created)\n', ' NavigatorState#4d6bf(lifecycle state: created)\n',
), ),
...@@ -1359,10 +1359,9 @@ void main() { ...@@ -1359,10 +1359,9 @@ void main() {
error.toStringDeep(), error.toStringDeep(),
equalsIgnoringHashCodes( equalsIgnoringHashCodes(
'FlutterError\n' 'FlutterError\n'
' A Navigator\'s onUnknownRoute returned null.\n' ' Navigator.onUnknownRoute returned null when requested to build\n'
' When trying to build the route "/", both onGenerateRoute and\n' ' route "/".\n'
' onUnknownRoute returned null. The onUnknownRoute callback should\n' ' The onUnknownRoute callback must never return null.\n'
' never return null.\n'
' The Navigator was:\n' ' The Navigator was:\n'
' NavigatorState#38036(lifecycle state: created)\n', ' NavigatorState#38036(lifecycle state: created)\n',
), ),
...@@ -1492,6 +1491,165 @@ void main() { ...@@ -1492,6 +1491,165 @@ void main() {
expect(tester.state<StatefulTestState>(find.byKey(topRoute)).rebuildCount, 1); expect(tester.state<StatefulTestState>(find.byKey(topRoute)).rebuildCount, 1);
}); });
testWidgets('initial routes below opaque route are offstage', (WidgetTester tester) async {
final GlobalKey<NavigatorState> g = GlobalKey<NavigatorState>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: g,
initialRoute: '/a/b',
onGenerateRoute: (RouteSettings s) {
return MaterialPageRoute<void>(
builder: (BuildContext c) {
return Text('+${s.name}+');
},
settings: s,
);
},
),
),
);
expect(find.text('+/+'), findsNothing);
expect(find.text('+/+', skipOffstage: false), findsOneWidget);
expect(find.text('+/a+'), findsNothing);
expect(find.text('+/a+', skipOffstage: false), findsOneWidget);
expect(find.text('+/a/b+'), findsOneWidget);
g.currentState.pop();
await tester.pumpAndSettle();
expect(find.text('+/+'), findsNothing);
expect(find.text('+/+', skipOffstage: false), findsOneWidget);
expect(find.text('+/a+'), findsOneWidget);
expect(find.text('+/a/b+'), findsNothing);
g.currentState.pop();
await tester.pumpAndSettle();
expect(find.text('+/+'), findsOneWidget);
expect(find.text('+/a+'), findsNothing);
expect(find.text('+/a/b+'), findsNothing);
});
testWidgets('Can provide custom onGenerateInitialRoutes', (WidgetTester tester) async {
bool onGenerateInitialRoutesCalled = false;
final GlobalKey<NavigatorState> g = GlobalKey<NavigatorState>();
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: g,
initialRoute: 'Hello World',
onGenerateInitialRoutes: (NavigatorState navigator, String initialRoute) {
onGenerateInitialRoutesCalled = true;
final List<Route<void>> result = <Route<void>>[];
for (final String route in initialRoute.split(' ')) {
result.add(MaterialPageRoute<void>(builder: (BuildContext context) {
return Text(route);
}));
}
return result;
},
),
),
);
expect(onGenerateInitialRoutesCalled, true);
expect(find.text('Hello'), findsNothing);
expect(find.text('World'), findsOneWidget);
g.currentState.pop();
await tester.pumpAndSettle();
expect(find.text('Hello'), findsOneWidget);
expect(find.text('World'), findsNothing);
});
testWidgets('pushAndRemove until animates the push', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/25080.
const Duration kFourTenthsOfTheTransitionDuration = Duration(milliseconds: 120);
final GlobalKey<NavigatorState> navigator = GlobalKey<NavigatorState>();
final Map<String, MaterialPageRoute<dynamic>> routeNameToContext = <String, MaterialPageRoute<dynamic>>{};
await tester.pumpWidget(
Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
key: navigator,
initialRoute: 'root',
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<void>(
settings: settings,
builder: (BuildContext context) {
routeNameToContext[settings.name] = ModalRoute.of(context) as MaterialPageRoute<dynamic>;
return Text('Route: ${settings.name}');
},
);
},
),
),
);
expect(find.text('Route: root'), findsOneWidget);
navigator.currentState.pushNamed('1');
await tester.pumpAndSettle();
expect(find.text('Route: 1'), findsOneWidget);
navigator.currentState.pushNamed('2');
await tester.pumpAndSettle();
expect(find.text('Route: 2'), findsOneWidget);
navigator.currentState.pushNamed('3');
await tester.pumpAndSettle();
expect(find.text('Route: 3'), findsOneWidget);
expect(find.text('Route: 2', skipOffstage: false), findsOneWidget);
expect(find.text('Route: 1', skipOffstage: false), findsOneWidget);
expect(find.text('Route: root', skipOffstage: false), findsOneWidget);
navigator.currentState.pushNamedAndRemoveUntil('4', (Route<dynamic> route) => route.isFirst);
await tester.pump();
expect(find.text('Route: 3'), findsOneWidget);
expect(find.text('Route: 4'), findsOneWidget);
final Animation<double> route4Entry = routeNameToContext['4'].animation;
expect(route4Entry.value, 0.0); // Entry animation has not started.
await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(find.text('Route: 3'), findsOneWidget);
expect(find.text('Route: 4'), findsOneWidget);
expect(route4Entry.value, 0.4);
await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(find.text('Route: 3'), findsOneWidget);
expect(find.text('Route: 4'), findsOneWidget);
expect(route4Entry.value, 0.8);
expect(find.text('Route: 2', skipOffstage: false), findsOneWidget);
expect(find.text('Route: 1', skipOffstage: false), findsOneWidget);
expect(find.text('Route: root', skipOffstage: false), findsOneWidget);
// When we hit 1.0 all but root and current have been removed.
await tester.pump(kFourTenthsOfTheTransitionDuration);
expect(find.text('Route: 3', skipOffstage: false), findsNothing);
expect(find.text('Route: 4'), findsOneWidget);
expect(route4Entry.value, 1.0);
expect(find.text('Route: 2', skipOffstage: false), findsNothing);
expect(find.text('Route: 1', skipOffstage: false), findsNothing);
expect(find.text('Route: root', skipOffstage: false), findsOneWidget);
navigator.currentState.pop();
await tester.pumpAndSettle();
expect(find.text('Route: root'), findsOneWidget);
expect(find.text('Route: 4', skipOffstage: false), findsNothing);
});
} }
class NoAnimationPageRoute extends PageRouteBuilder<void> { class NoAnimationPageRoute extends PageRouteBuilder<void> {
......
...@@ -28,16 +28,15 @@ class TestRoute extends Route<String> with LocalHistoryRoute<String> { ...@@ -28,16 +28,15 @@ class TestRoute extends Route<String> with LocalHistoryRoute<String> {
} }
@override @override
void install(OverlayEntry insertionPoint) { void install() {
log('install'); log('install');
final OverlayEntry entry = OverlayEntry( final OverlayEntry entry = OverlayEntry(
builder: (BuildContext context) => Container(), builder: (BuildContext context) => Container(),
opaque: true, opaque: true,
); );
_entries.add(entry); _entries.add(entry);
navigator.overlay?.insert(entry, above: insertionPoint);
routes.add(this); routes.add(this);
super.install(insertionPoint); super.install();
} }
@override @override
...@@ -46,6 +45,12 @@ class TestRoute extends Route<String> with LocalHistoryRoute<String> { ...@@ -46,6 +45,12 @@ class TestRoute extends Route<String> with LocalHistoryRoute<String> {
return super.didPush(); return super.didPush();
} }
@override
void didAdd() {
log('didAdd');
super.didAdd();
}
@override @override
void didReplace(Route<dynamic> oldRoute) { void didReplace(Route<dynamic> oldRoute) {
expect(oldRoute, isA<TestRoute>()); expect(oldRoute, isA<TestRoute>());
...@@ -82,8 +87,6 @@ class TestRoute extends Route<String> with LocalHistoryRoute<String> { ...@@ -82,8 +87,6 @@ class TestRoute extends Route<String> with LocalHistoryRoute<String> {
@override @override
void dispose() { void dispose() {
log('dispose'); log('dispose');
for (final OverlayEntry entry in _entries)
entry.remove();
_entries.clear(); _entries.clear();
routes.remove(this); routes.remove(this);
super.dispose(); super.dispose();
...@@ -110,10 +113,6 @@ void main() { ...@@ -110,10 +113,6 @@ void main() {
expect(settings, hasOneLineDescription); expect(settings, hasOneLineDescription);
final RouteSettings settings2 = settings.copyWith(name: 'B'); final RouteSettings settings2 = settings.copyWith(name: 'B');
expect(settings2.name, 'B'); expect(settings2.name, 'B');
expect(settings2.isInitialRoute, false);
final RouteSettings settings3 = settings2.copyWith(isInitialRoute: true);
expect(settings3.name, 'B');
expect(settings3.isInitialRoute, true);
}); });
testWidgets('Route settings arguments', (WidgetTester tester) async { testWidgets('Route settings arguments', (WidgetTester tester) async {
...@@ -133,7 +132,7 @@ void main() { ...@@ -133,7 +132,7 @@ void main() {
expect(settings4.arguments, isNot(same(arguments))); expect(settings4.arguments, isNot(same(arguments)));
}); });
testWidgets('Route management - push, replace, pop', (WidgetTester tester) async { testWidgets('Route management - push, replace, pop sequence', (WidgetTester tester) async {
final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>(); final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
await tester.pumpWidget( await tester.pumpWidget(
Directionality( Directionality(
...@@ -151,7 +150,7 @@ void main() { ...@@ -151,7 +150,7 @@ void main() {
() { }, () { },
<String>[ <String>[
'initial: install', 'initial: install',
'initial: didPush', 'initial: didAdd',
'initial: didChangeNext null', 'initial: didChangeNext null',
], ],
); );
...@@ -160,7 +159,7 @@ void main() { ...@@ -160,7 +159,7 @@ void main() {
tester, tester,
host, host,
() { host.push(second = TestRoute('second')); }, () { host.push(second = TestRoute('second')); },
<String>[ <String>[ // stack is: initial, second
'second: install', 'second: install',
'second: didPush', 'second: didPush',
'second: didChangeNext null', 'second: didChangeNext null',
...@@ -171,7 +170,7 @@ void main() { ...@@ -171,7 +170,7 @@ void main() {
tester, tester,
host, host,
() { host.push(TestRoute('third')); }, () { host.push(TestRoute('third')); },
<String>[ <String>[ // stack is: initial, second, third
'third: install', 'third: install',
'third: didPush', 'third: didPush',
'third: didChangeNext null', 'third: didChangeNext null',
...@@ -182,7 +181,7 @@ void main() { ...@@ -182,7 +181,7 @@ void main() {
tester, tester,
host, host,
() { host.replace(oldRoute: second, newRoute: TestRoute('two')); }, () { host.replace(oldRoute: second, newRoute: TestRoute('two')); },
<String>[ <String>[ // stack is: initial, two, third
'two: install', 'two: install',
'two: didReplace second', 'two: didReplace second',
'two: didChangeNext third', 'two: didChangeNext third',
...@@ -194,20 +193,20 @@ void main() { ...@@ -194,20 +193,20 @@ void main() {
tester, tester,
host, host,
() { host.pop('hello'); }, () { host.pop('hello'); },
<String>[ <String>[ // stack is: initial, two
'third: didPop hello', 'third: didPop hello',
'third: dispose',
'two: didPopNext third', 'two: didPopNext third',
'third: dispose',
], ],
); );
await runNavigatorTest( await runNavigatorTest(
tester, tester,
host, host,
() { host.pop('good bye'); }, () { host.pop('good bye'); },
<String>[ <String>[ // stack is: initial
'two: didPop good bye', 'two: didPop good bye',
'two: dispose',
'initial: didPopNext two', 'initial: didPopNext two',
'two: dispose',
], ],
); );
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
...@@ -234,7 +233,7 @@ void main() { ...@@ -234,7 +233,7 @@ void main() {
() { }, () { },
<String>[ <String>[
'first: install', 'first: install',
'first: didPush', 'first: didAdd',
'first: didChangeNext null', 'first: didChangeNext null',
], ],
); );
...@@ -275,8 +274,8 @@ void main() { ...@@ -275,8 +274,8 @@ void main() {
() { host.pop('good bye'); }, () { host.pop('good bye'); },
<String>[ <String>[
'third: didPop good bye', 'third: didPop good bye',
'third: dispose',
'second: didPopNext third', 'second: didPopNext third',
'third: dispose',
], ],
); );
await runNavigatorTest( await runNavigatorTest(
...@@ -317,8 +316,8 @@ void main() { ...@@ -317,8 +316,8 @@ void main() {
() { host.pop('the end'); }, () { host.pop('the end'); },
<String>[ <String>[
'four: didPop the end', 'four: didPop the end',
'four: dispose',
'second: didPopNext four', 'second: didPopNext four',
'four: dispose',
], ],
); );
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
...@@ -345,7 +344,7 @@ void main() { ...@@ -345,7 +344,7 @@ void main() {
() { }, () { },
<String>[ <String>[
'A: install', 'A: install',
'A: didPush', 'A: didAdd',
'A: didChangeNext null', 'A: didChangeNext null',
], ],
); );
...@@ -392,8 +391,8 @@ void main() { ...@@ -392,8 +391,8 @@ void main() {
() { host.popUntil((Route<dynamic> route) => route == routeB); }, () { host.popUntil((Route<dynamic> route) => route == routeB); },
<String>[ <String>[
'C: didPop null', 'C: didPop null',
'C: dispose',
'b: didPopNext C', 'b: didPopNext C',
'C: dispose',
], ],
); );
await tester.pumpWidget(Container()); await tester.pumpWidget(Container());
...@@ -427,7 +426,7 @@ void main() { ...@@ -427,7 +426,7 @@ void main() {
() { host.popUntil((Route<dynamic> route) => !route.willHandlePopInternally); }, () { host.popUntil((Route<dynamic> route) => !route.willHandlePopInternally); },
<String>[ <String>[
'A: install', 'A: install',
'A: didPush', 'A: didAdd',
'A: didChangeNext null', 'A: didChangeNext null',
'A: didPop null', 'A: didPop null',
'A: onRemove 1', 'A: onRemove 1',
......
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