Unverified Commit fc85492d authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Make Navigator restorable (inkl. WidgetsApp, MaterialApp, CupertinoApp) (#65658)

parent 5d6321b5
......@@ -94,6 +94,7 @@ class CupertinoApp extends StatefulWidget {
this.debugShowCheckedModeBanner = true,
this.shortcuts,
this.actions,
this.restorationScopeId,
}) : assert(routes != null),
assert(navigatorObservers != null),
assert(title != null),
......@@ -132,6 +133,7 @@ class CupertinoApp extends StatefulWidget {
this.debugShowCheckedModeBanner = true,
this.shortcuts,
this.actions,
this.restorationScopeId,
}) : assert(title != null),
assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null),
......@@ -315,6 +317,9 @@ class CupertinoApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
final Map<Type, Action<Intent>>? actions;
/// {@macro flutter.widgets.widgetsApp.restorationScopeId}
final String? restorationScopeId;
@override
_CupertinoAppState createState() => _CupertinoAppState();
......@@ -400,6 +405,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts,
actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
);
}
return WidgetsApp(
......@@ -433,6 +439,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts,
actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
);
}
......
......@@ -197,6 +197,7 @@ class MaterialApp extends StatefulWidget {
this.debugShowCheckedModeBanner = true,
this.shortcuts,
this.actions,
this.restorationScopeId,
}) : assert(routes != null),
assert(navigatorObservers != null),
assert(title != null),
......@@ -241,6 +242,7 @@ class MaterialApp extends StatefulWidget {
this.debugShowCheckedModeBanner = true,
this.shortcuts,
this.actions,
this.restorationScopeId,
}) : assert(routeInformationParser != null),
assert(routerDelegate != null),
assert(title != null),
......@@ -620,6 +622,9 @@ class MaterialApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
final Map<Type, Action<Intent>> actions;
/// {@macro flutter.widgets.widgetsApp.restorationScopeId}
final String restorationScopeId;
/// Turns on a [GridPaper] overlay that paints a baseline grid
/// Material apps.
///
......@@ -780,6 +785,7 @@ class _MaterialAppState extends State<MaterialApp> {
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts,
actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
);
}
......@@ -814,6 +820,7 @@ class _MaterialAppState extends State<MaterialApp> {
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts,
actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
);
}
......
......@@ -19,6 +19,7 @@ import 'media_query.dart';
import 'navigator.dart';
import 'pages.dart';
import 'performance_overlay.dart';
import 'restoration.dart';
import 'router.dart';
import 'scrollable.dart';
import 'semantics_debugger.dart';
......@@ -195,6 +196,7 @@ class WidgetsApp extends StatefulWidget {
this.inspectorSelectButtonBuilder,
this.shortcuts,
this.actions,
this.restorationScopeId,
}) : assert(navigatorObservers != null),
assert(routes != null),
assert(
......@@ -290,6 +292,7 @@ class WidgetsApp extends StatefulWidget {
this.inspectorSelectButtonBuilder,
this.shortcuts,
this.actions,
this.restorationScopeId,
}) : assert(
routeInformationParser != null &&
routerDelegate != null,
......@@ -945,6 +948,24 @@ class WidgetsApp extends StatefulWidget {
/// {@endtemplate}
final Map<Type, Action<Intent>>? actions;
/// {@template flutter.widgets.widgetsApp.restorationScopeId}
/// The identifier to use for state restoration of this app.
///
/// Providing a restoration ID inserts a [RootRestorationScope] into the
/// widget hierarchy, which enables state restoration for descendant widgets.
///
/// Providing a restoration ID also enables the [Navigator] built by the
/// [WidgetsApp] to restore its state (i.e. to restore the history stack of
/// active [Route]s). See the documentation on [Navigator] for more details
/// around state restoration of [Route]s.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
/// {@endtemplate}
final String? restorationScopeId;
/// If true, forces the performance overlay to be visible in all instances.
///
/// Used by the `showPerformanceOverlay` observatory extension.
......@@ -1467,6 +1488,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
} else {
assert(_navigator != null);
routing = Navigator(
restorationScopeId: 'nav',
key: _navigator,
initialRoute: _initialRouteName,
onGenerateRoute: _onGenerateRoute,
......@@ -1573,18 +1595,21 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
: _locale!;
assert(_debugCheckLocalizations(appLocale));
return Shortcuts(
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
debugLabel: '<Default WidgetsApp Shortcuts>',
child: Actions(
actions: widget.actions ?? WidgetsApp.defaultActions,
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: _MediaQueryFromWindow(
child: Localizations(
locale: appLocale,
delegates: _localizationsDelegates.toList(),
child: title,
return RootRestorationScope(
restorationId: widget.restorationScopeId,
child: Shortcuts(
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
debugLabel: '<Default WidgetsApp Shortcuts>',
child: Actions(
actions: widget.actions ?? WidgetsApp.defaultActions,
child: FocusTraversalGroup(
policy: ReadingOrderTraversalPolicy(),
child: _MediaQueryFromWindow(
child: Localizations(
locale: appLocale,
delegates: _localizationsDelegates.toList(),
child: title,
),
),
),
),
......
......@@ -6,6 +6,7 @@ import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:developer' as developer;
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
......@@ -19,6 +20,8 @@ import 'focus_scope.dart';
import 'framework.dart';
import 'heroes.dart';
import 'overlay.dart';
import 'restoration.dart';
import 'restoration_properties.dart';
import 'routes.dart';
import 'ticker_provider.dart';
......@@ -42,6 +45,18 @@ typedef RouteFactory = Route<dynamic>? Function(RouteSettings settings);
/// Used by [Navigator.onGenerateInitialRoutes].
typedef RouteListFactory = List<Route<dynamic>> Function(NavigatorState navigator, String initialRoute);
/// Creates a [Route] that is to be added to a [Navigator].
///
/// The route can be configured with the provided `arguments`. The provided
/// `context` is the `BuildContext` of the [Navigator] to which the route is
/// added.
///
/// Used by the restorable methods of the [Navigator] that add anonymous routes
/// (e.g. [NavigatorState.restorablePush]). For this use case, the
/// [RestorableRouteBuilder] must be static function as the [Navigator] will
/// call it again during state restoration to re-create the route.
typedef RestorableRouteBuilder<T> = Route<T> Function(BuildContext context, Object? arguments);
/// Signature for the [Navigator.popUntil] predicate argument.
typedef RoutePredicate = bool Function(Route<dynamic> route);
......@@ -137,6 +152,21 @@ abstract class Route<T> {
RouteSettings get settings => _settings;
RouteSettings _settings;
/// The restoration scope ID to be used for the [RestorationScope] surrounding
/// this route.
///
/// The restoration scope ID is null if restoration is currently disabled
/// for this route.
///
/// If the restoration scope ID changes (e.g. because restoration is enabled
/// or disabled) during the life of the route, the [ValueListenable] notifies
/// its listeners. As an example, the ID changes to null while the route is
/// transitioning off screen, which triggers a notification on this field. At
/// that point, the route is considered as no longer present for restoration
/// purposes and its state will not be restored.
ValueListenable<String?> get restorationScopeId => _restorationScopeId;
final ValueNotifier<String?> _restorationScopeId = ValueNotifier<String?>(null);
void _updateSettings(RouteSettings newSettings) {
assert(newSettings != null);
if (_settings != newSettings) {
......@@ -145,6 +175,10 @@ abstract class Route<T> {
}
}
void _updateRestorationId(String? restorationId) {
_restorationScopeId.value = restorationId;
}
/// The overlay entries of this route.
///
/// These are typically populated by [install]. The [Navigator] is in charge
......@@ -510,6 +544,7 @@ abstract class Page<T> extends RouteSettings {
this.key,
String? name,
Object? arguments,
this.restorationId,
}) : super(name: name, arguments: arguments);
/// The key associated with this page.
......@@ -517,6 +552,17 @@ abstract class Page<T> extends RouteSettings {
/// This key will be used for comparing pages in [canUpdate].
final LocalKey? key;
/// Restoration ID to save and restore the state of the [Route] configured by
/// this page.
///
/// If no restoration ID is provided, the [Route] will not restore its state.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
final String? restorationId;
/// Whether this page can be updated with the [other] page.
///
/// Two pages are consider updatable if they have same the [runtimeType] and
......@@ -1354,6 +1400,30 @@ class DefaultTransitionDelegate<T> extends TransitionDelegate<T> {
/// [Navigator], especially in large `build` methods where nested [Navigator]s
/// are created. The [Builder] widget can be used to access a [BuildContext] at
/// a desired location in the widget subtree.
///
/// ## State Restoration
///
/// If provided with a [restorationScopeId] and when surrounded by a valid
/// [RestorationScope] the [Navigator] will restore its state by recreating
/// the current history stack of [Route]s during state restoration and by
/// restoring the internal state of those [Route]s. However, not all [Route]s
/// on the stack can be restored:
///
/// * [Page]-based routes restore their state if [Page.restorationId] is
/// provided.
/// * [Route]s added with the classic imperative API ([push], [pushNamed], and
/// friends) can never restore their state.
/// * A [Route] added with the restorable imperative API ([restorablePush],
/// [restorablePushNamed], and all other imperative methods with "restorable"
/// in their name) restores its state if all routes below it up to and
/// including the first [Page]-based route below it are restored. If there
/// is no [Page]-based route below it, it only restores its state if all
/// routes below it restore theirs.
///
/// If a [Route] is deemed restorable, the [Navigator] will set its
/// [Route.restorationScopeId] to a non-null value. Routes can use that ID to
/// store and restore their own state. As an example, the [ModalRoute] will
/// use this ID to create a [RestorationScope] for its content widgets.
class Navigator extends StatefulWidget {
/// Creates a widget that maintains a stack-based history of child widgets.
///
......@@ -1372,6 +1442,7 @@ class Navigator extends StatefulWidget {
this.transitionDelegate = const DefaultTransitionDelegate<dynamic>(),
this.reportsRouteUpdateToEngine = false,
this.observers = const <NavigatorObserver>[],
this.restorationScopeId,
}) : assert(pages != null),
assert(onGenerateInitialRoutes != null),
assert(transitionDelegate != null),
......@@ -1454,6 +1525,26 @@ class Navigator extends StatefulWidget {
/// A list of observers for this navigator.
final List<NavigatorObserver> observers;
/// Restoration ID to save and restore the state of the navigator, including
/// its history.
///
/// If a restoration ID is provided, the navigator will persist its internal
/// state (including the route history as well as the restorable state of the
/// routes) and restore it during state restoration.
///
/// If no restoration ID is provided, the route history stack will not be
/// restored and state restoration is disabled for the individual routes as
/// well.
///
/// The state is persisted in a [RestorationBucket] claimed from
/// the surrounding [RestorationScope] using the provided restoration ID.
///
/// See also:
///
/// * [RestorationManager], which explains how state restoration works in
/// Flutter.
final String? restorationScopeId;
/// The name for the default route of the application.
///
/// See also:
......@@ -1576,6 +1667,11 @@ class Navigator extends StatefulWidget {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePushNamed], which pushes a route that can be restored
/// during state restoration.
@optionalTypeArgs
static Future<T> pushNamed<T extends Object?>(
BuildContext context,
......@@ -1585,6 +1681,61 @@ class Navigator extends StatefulWidget {
return Navigator.of(context)!.pushNamed<T>(routeName, arguments: arguments);
}
/// Push a named route onto the navigator that most tightly encloses the given
/// context.
///
/// {@template flutter.widgets.navigator.restorablePushNamed}
/// Unlike [Route]s pushed via [pushNamed], [Route]s pushed with this method
/// are restored during state restoration according to the rules outlined
/// in the "State Restoration" section of [Navigator].
/// {@endtemplate}
///
/// {@macro flutter.widgets.navigator.pushNamed}
///
/// {@template flutter.widgets.navigator.restorablePushNamed.arguments}
/// The provided `arguments` are passed to the pushed route via
/// [RouteSettings.arguments]. Any object that is serializable via the
/// [StandardMessageCodec] can be passed as `arguments`. Often, a Map is used
/// to pass key-value pairs.
///
/// The arguments may be used in [Navigator.onGenerateRoute] or
/// [Navigator.onUnknownRoute] to construct the route.
/// {@endtemplate}
///
/// {@template flutter.widgets.navigator.restorablePushNamed.returnValue}
/// The method returns an opaque ID for the pushed route that can be used by
/// the [RestorableRouteFuture] to gain access to the actual [Route] object
/// added to the navigator and its return value. You can ignore the return
/// value of this method, if you do not care about the route object or the
/// route's return value.
/// {@endtemplate}
///
/// {@tool snippet}
///
/// Typical usage is as follows:
///
/// ```dart
/// void _showParisWeather() {
/// Navigator.restorablePushNamed(
/// context,
/// '/weather',
/// arguments: <String, String>{
/// 'city': 'Paris',
/// 'country': 'France',
/// },
/// );
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
static String restorablePushNamed<T extends Object?>(
BuildContext context,
String routeName, {
Object? arguments,
}) {
return Navigator.of(context)!.restorablePushNamed<T>(routeName, arguments: arguments);
}
/// Replace the current route of the navigator that most tightly encloses the
/// given context by pushing the route named [routeName] and then disposing
/// the previous route once the new route has finished animating in.
......@@ -1633,6 +1784,11 @@ class Navigator extends StatefulWidget {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePushReplacementNamed], which pushes a replacement route that
/// can be restored during state restoration.
@optionalTypeArgs
static Future<T> pushReplacementNamed<T extends Object?, TO extends Object?>(
BuildContext context,
......@@ -1643,6 +1799,42 @@ class Navigator extends StatefulWidget {
return Navigator.of(context)!.pushReplacementNamed<T, TO>(routeName, arguments: arguments, result: result);
}
/// Replace the current route of the navigator that most tightly encloses the
/// given context by pushing the route named [routeName] and then disposing
/// the previous route once the new route has finished animating in.
///
/// {@template flutter.widgets.navigator.restorablePushReplacementNamed}
/// Unlike [Route]s pushed via [pushReplacementNamed], [Route]s pushed with
/// this method are restored during state restoration according to the rules
/// outlined in the "State Restoration" section of [Navigator].
/// {@endtemplate}
///
/// {@macro flutter.widgets.navigator.pushReplacementNamed}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool snippet}
///
/// Typical usage is as follows:
///
/// ```dart
/// void _switchToAudioVolume() {
/// Navigator.restorablePushReplacementNamed(context, '/settings/volume');
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
static String restorablePushReplacementNamed<T extends Object?, TO extends Object?>(
BuildContext context,
String routeName, {
TO? result,
Object? arguments,
}) {
return Navigator.of(context)!.restorablePushReplacementNamed<T, TO>(routeName, arguments: arguments, result: result);
}
/// Pop the current route off the navigator that most tightly encloses the
/// given context and push a named route in its place.
///
......@@ -1686,6 +1878,11 @@ class Navigator extends StatefulWidget {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePopAndPushNamed], which pushes a new route that can be
/// restored during state restoration.
@optionalTypeArgs
static Future<T> popAndPushNamed<T extends Object?, TO extends Object?>(
BuildContext context,
......@@ -1696,6 +1893,41 @@ class Navigator extends StatefulWidget {
return Navigator.of(context)!.popAndPushNamed<T, TO>(routeName, arguments: arguments, result: result);
}
/// Pop the current route off the navigator that most tightly encloses the
/// given context and push a named route in its place.
///
/// {@template flutter.widgets.navigator.restorablePopAndPushNamed}
/// Unlike [Route]s pushed via [popAndPushNamed], [Route]s pushed with
/// this method are restored during state restoration according to the rules
/// outlined in the "State Restoration" section of [Navigator].
/// {@endtemplate}
///
/// {@macro flutter.widgets.navigator.popAndPushNamed}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool snippet}
///
/// Typical usage is as follows:
///
/// ```dart
/// void _selectNetwork() {
/// Navigator.restorablePopAndPushNamed(context, '/settings/network');
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
static String restorablePopAndPushNamed<T extends Object?, TO extends Object?>(
BuildContext context,
String routeName, {
TO? result,
Object? arguments,
}) {
return Navigator.of(context)!.restorablePopAndPushNamed<T, TO>(routeName, arguments: arguments, result: result);
}
/// Push the route with the given name onto the navigator that most tightly
/// encloses the given context, and then remove all the previous routes until
/// the `predicate` returns true.
......@@ -1750,6 +1982,11 @@ class Navigator extends StatefulWidget {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePushNamedAndRemoveUntil], which pushes a new route that can
/// be restored during state restoration.
@optionalTypeArgs
static Future<T> pushNamedAndRemoveUntil<T extends Object?>(
BuildContext context,
......@@ -1760,6 +1997,42 @@ class Navigator extends StatefulWidget {
return Navigator.of(context)!.pushNamedAndRemoveUntil<T>(newRouteName, predicate, arguments: arguments);
}
/// Push the route with the given name onto the navigator that most tightly
/// encloses the given context, and then remove all the previous routes until
/// the `predicate` returns true.
///
/// {@template flutter.widgets.navigator.restorablePushNamedAndRemoveUntil}
/// Unlike [Route]s pushed via [pushNamedAndRemoveUntil], [Route]s pushed with
/// this method are restored during state restoration according to the rules
/// outlined in the "State Restoration" section of [Navigator].
/// {@endtemplate}
///
/// {@macro flutter.widgets.navigator.pushNamedAndRemoveUntil}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool snippet}
///
/// Typical usage is as follows:
///
/// ```dart
/// void _resetToOverview() {
/// Navigator.restorablePushNamedAndRemoveUntil(context, '/overview', ModalRoute.withName('/'));
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
static String restorablePushNamedAndRemoveUntil<T extends Object?>(
BuildContext context,
String newRouteName,
RoutePredicate predicate, {
Object? arguments,
}) {
return Navigator.of(context)!.restorablePushNamedAndRemoveUntil<T>(newRouteName, predicate, arguments: arguments);
}
/// Push the given route onto the navigator that most tightly encloses the
/// given context.
///
......@@ -1788,11 +2061,70 @@ class Navigator extends StatefulWidget {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePush], which pushes a route that can be restored during
/// state restoration.
@optionalTypeArgs
static Future<T> push<T extends Object?>(BuildContext context, Route<T> route) {
return Navigator.of(context)!.push(route);
}
/// Push a new route onto the navigator that most tightly encloses the
/// given context.
///
/// {@template flutter.widgets.navigator.restorablePush}
/// Unlike [Route]s pushed via [push], [Route]s pushed with this method are
/// restored during state restoration according to the rules outlined in the
/// "State Restoration" section of [Navigator].
/// {@endtemplate}
///
/// {@macro flutter.widgets.navigator.push}
///
/// {@template flutter.widgets.navigator.restorablePush.arguments}
/// The method takes a _static_ [RestorableRouteBuilder] as argument, which
/// must instantiate and return a new [Route] object that will be added to
/// the navigator. The provided `arguments` object is passed to the
/// `routeBuilder`. The navigator calls the static `routeBuilder` function
/// again during state restoration to re-create the route object.
///
/// Any object that is serializable via the [StandardMessageCodec] can be
/// passed as `arguments`. Often, a Map is used to pass key-value pairs.
/// {@endtemplate}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// Typical usage is as follows:
///
/// ```dart
/// static Route _myRouteBuilder(BuildContext context, Object arguments) {
/// return MaterialPageRoute(
/// builder: (BuildContext context) => MyStatefulWidget(),
/// );
/// }
///
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: const Text('Sample Code'),
/// ),
/// floatingActionButton: FloatingActionButton(
/// onPressed: () => Navigator.restorablePush(context, _myRouteBuilder),
/// tooltip: 'Increment Counter',
/// child: const Icon(Icons.add),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
static String restorablePush<T extends Object?>(BuildContext context, RestorableRouteBuilder<T> routeBuilder, {Object? arguments}) {
return Navigator.of(context)!.restorablePush(routeBuilder, arguments: arguments);
}
/// Replace the current route of the navigator that most tightly encloses the
/// given context by pushing the given route and then disposing the previous
/// route once the new route has finished animating in.
......@@ -1832,11 +2164,62 @@ class Navigator extends StatefulWidget {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePushReplacement], which pushes a replacement route that can
/// be restored during state restoration.
@optionalTypeArgs
static Future<T> pushReplacement<T extends Object?, TO extends Object?>(BuildContext context, Route<T> newRoute, { TO? result }) {
return Navigator.of(context)!.pushReplacement<T, TO>(newRoute, result: result);
}
/// Replace the current route of the navigator that most tightly encloses the
/// given context by pushing a new route and then disposing the previous
/// route once the new route has finished animating in.
///
/// {@template flutter.widgets.navigator.restorablePushReplacement}
/// Unlike [Route]s pushed via [pushReplacement], [Route]s pushed with this
/// method are restored during state restoration according to the rules
/// outlined in the "State Restoration" section of [Navigator].
/// {@endtemplate}
///
/// {@macro flutter.widgets.navigator.pushReplacement}
///
/// {@macro flutter.widgets.navigator.restorablePush.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// Typical usage is as follows:
///
/// ```dart
/// static Route _myRouteBuilder(BuildContext context, Object arguments) {
/// return MaterialPageRoute(
/// builder: (BuildContext context) => MyStatefulWidget(),
/// );
/// }
///
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: const Text('Sample Code'),
/// ),
/// floatingActionButton: FloatingActionButton(
/// onPressed: () => Navigator.restorablePushReplacement(context, _myRouteBuilder),
/// tooltip: 'Increment Counter',
/// child: const Icon(Icons.add),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
static String restorablePushReplacement<T extends Object?, TO extends Object?>(BuildContext context, RestorableRouteBuilder<T> routeBuilder, { TO? result, Object? arguments }) {
return Navigator.of(context)!.restorablePushReplacement<T, TO>(routeBuilder, result: result, arguments: arguments);
}
/// Push the given route onto the navigator that most tightly encloses the
/// given context, and then remove all the previous routes until the
/// `predicate` returns true.
......@@ -1886,11 +2269,66 @@ class Navigator extends StatefulWidget {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePushAndRemoveUntil], which pushes a route that can be
/// restored during state restoration.
@optionalTypeArgs
static Future<T> pushAndRemoveUntil<T extends Object?>(BuildContext context, Route<T> newRoute, RoutePredicate predicate) {
return Navigator.of(context)!.pushAndRemoveUntil<T>(newRoute, predicate);
}
/// Push a new route onto the navigator that most tightly encloses the
/// given context, and then remove all the previous routes until the
/// `predicate` returns true.
///
/// {@template flutter.widgets.navigator.restorablePushAndRemoveUntil}
/// Unlike [Route]s pushed via [pushAndRemoveUntil], [Route]s pushed with this
/// method are restored during state restoration according to the rules
/// outlined in the "State Restoration" section of [Navigator].
/// {@endtemplate}
///
/// {@macro flutter.widgets.navigator.pushAndRemoveUntil}
///
/// {@macro flutter.widgets.navigator.restorablePush.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// Typical usage is as follows:
///
/// ```dart
/// static Route _myRouteBuilder(BuildContext context, Object arguments) {
/// return MaterialPageRoute(
/// builder: (BuildContext context) => MyStatefulWidget(),
/// );
/// }
///
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: const Text('Sample Code'),
/// ),
/// floatingActionButton: FloatingActionButton(
/// onPressed: () => Navigator.restorablePushAndRemoveUntil(
/// context,
/// _myRouteBuilder,
/// ModalRoute.withName('/'),
/// ),
/// tooltip: 'Increment Counter',
/// child: const Icon(Icons.add),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
static String restorablePushAndRemoveUntil<T extends Object?>(BuildContext context, RestorableRouteBuilder<T> newRouteBuilder, RoutePredicate predicate, {Object? arguments}) {
return Navigator.of(context)!.restorablePushAndRemoveUntil<T>(newRouteBuilder, predicate, arguments: arguments);
}
/// Replaces a route on the navigator that most tightly encloses the given
/// context with a new route.
///
......@@ -1922,11 +2360,32 @@ class Navigator extends StatefulWidget {
///
/// * [replaceRouteBelow], which is the same but identifies the route to be
/// removed by reference to the route above it, rather than directly.
/// * [restorableReplace], which adds a replacement route that can be
/// restored during state restoration.
@optionalTypeArgs
static void replace<T extends Object?>(BuildContext context, { required Route<dynamic> oldRoute, required Route<T> newRoute }) {
return Navigator.of(context)!.replace<T>(oldRoute: oldRoute, newRoute: newRoute);
}
/// Replaces a route on the navigator that most tightly encloses the given
/// context with a new route.
///
/// {@template flutter.widgets.navigator.restorableReplace}
/// Unlike [Route]s added via [replace], [Route]s added with this method are
/// restored during state restoration according to the rules outlined in the
/// "State Restoration" section of [Navigator].
/// {@endtemplate}
///
/// {@macro flutter.widgets.navigator.replace}
///
/// {@macro flutter.widgets.navigator.restorablePush.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
@optionalTypeArgs
static String restorableReplace<T extends Object?>(BuildContext context, { required Route<dynamic> oldRoute, required RestorableRouteBuilder<T> newRouteBuilder, Object? arguments }) {
return Navigator.of(context)!.restorableReplace<T>(oldRoute: oldRoute, newRouteBuilder: newRouteBuilder, arguments: arguments);
}
/// Replaces a route on the navigator that most tightly encloses the given
/// context with a new route. The route to be replaced is the one below the
/// given `anchorRoute`.
......@@ -1956,11 +2415,33 @@ class Navigator extends StatefulWidget {
///
/// * [replace], which is the same but identifies the route to be removed
/// directly.
/// * [restorableReplaceRouteBelow], which adds a replacement route that can
/// be restored during state restoration.
@optionalTypeArgs
static void replaceRouteBelow<T extends Object?>(BuildContext context, { required Route<dynamic> anchorRoute, required Route<T> newRoute }) {
return Navigator.of(context)!.replaceRouteBelow<T>(anchorRoute: anchorRoute, newRoute: newRoute);
}
/// Replaces a route on the navigator that most tightly encloses the given
/// context with a new route. The route to be replaced is the one below the
/// given `anchorRoute`.
///
/// {@template flutter.widgets.navigator.restorableReplaceRouteBelow}
/// Unlike [Route]s added via [restorableReplaceRouteBelow], [Route]s added
/// with this method are restored during state restoration according to the
/// rules outlined in the "State Restoration" section of [Navigator].
/// {@endtemplate}
///
/// {@macro flutter.widgets.navigator.replaceRouteBelow}
///
/// {@macro flutter.widgets.navigator.restorablePush.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
@optionalTypeArgs
static String restorableReplaceRouteBelow<T extends Object?>(BuildContext context, { required Route<dynamic> anchorRoute, required RestorableRouteBuilder<T> newRouteBuilder, Object? arguments }) {
return Navigator.of(context)!.restorableReplaceRouteBelow<T>(anchorRoute: anchorRoute, newRouteBuilder: newRouteBuilder, arguments: arguments);
}
/// Whether the navigator that most tightly encloses the given context can be
/// popped.
///
......@@ -2346,6 +2827,7 @@ class _RouteEntry extends RouteTransitionRecord {
_RouteEntry(
this.route, {
required _RouteLifecycle initialState,
this.restorationInformation,
}) : assert(route != null),
assert(initialState != null),
assert(
......@@ -2359,6 +2841,7 @@ class _RouteEntry extends RouteTransitionRecord {
@override
final Route<dynamic> route;
final _RestorationInformation? restorationInformation;
static Route<dynamic> notAnnounced = _NotAnnounced();
......@@ -2367,6 +2850,21 @@ class _RouteEntry extends RouteTransitionRecord {
Route<dynamic> lastAnnouncedPoppedNextRoute = notAnnounced; // last argument to Route.didPopNext
Route<dynamic>? lastAnnouncedNextRoute = notAnnounced; // last argument to Route.didChangeNext
/// Restoration ID to be used for the encapsulating route when restoration is
/// enabled for it or null if restoration cannot be enabled for it.
String? get restorationId {
// User-provided restoration ids of Pages are prefixed with 'p+'. Generated
// ids for pageless routes are prefixed with 'r+' to avoid clashes.
if (hasPage) {
final Page<Object> page = route.settings as Page<Object>;
return page.restorationId != null ? 'p+${page.restorationId}' : null;
}
if (restorationInformation != null) {
return 'r+${restorationInformation!.restorationScopeId}';
}
return null;
}
bool get hasPage => route.settings is Page;
bool canUpdateFrom(Page<dynamic> page) {
......@@ -2537,6 +3035,8 @@ class _RouteEntry extends RouteTransitionRecord {
currentState.index >= _RouteLifecycle.add.index;
}
bool get isPresentForRestoration => currentState.index <= _RouteLifecycle.idle.index;
bool get suitableForAnnouncement {
return currentState.index <= _RouteLifecycle.removing.index &&
currentState.index >= _RouteLifecycle.push.index;
......@@ -2629,6 +3129,12 @@ class _RouteEntry extends RouteTransitionRecord {
remove();
_isWaitingForExitingDecision = false;
}
bool get restorationEnabled => route.restorationScopeId.value != null;
set restorationEnabled(bool value) {
assert(!value || restorationId != null);
route._updateRestorationId(value ? restorationId : null);
}
}
abstract class _NavigatorObservation {
......@@ -2693,9 +3199,10 @@ class _NavigatorReplaceObservation extends _NavigatorObservation {
/// The state for a [Navigator] widget.
///
/// A reference to this class can be obtained by calling [Navigator.of].
class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
final GlobalKey<OverlayState> _overlayKey = GlobalKey<OverlayState>();
class NavigatorState extends State<Navigator> with TickerProviderStateMixin, RestorationMixin {
late GlobalKey<OverlayState> _overlayKey;
List<_RouteEntry> _history = <_RouteEntry>[];
final _HistoryProperty _serializableHistory = _HistoryProperty();
final Queue<_NavigatorObservation> _observedRouteAdditions = Queue<_NavigatorObservation>();
final Queue<_NavigatorObservation> _observedRouteDeletions = Queue<_NavigatorObservation>();
......@@ -2727,39 +3234,86 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
.getElementForInheritedWidgetOfExactType<HeroControllerScope>()
?.widget as HeroControllerScope?;
_updateHeroController(heroControllerScope?.controller);
}
// Use [_nextPagelessRestorationScopeId] to get the next id.
final RestorableNum<int> _rawNextPagelessRestorationScopeId = RestorableNum<int>(0);
String? initialRoute = widget.initialRoute;
if (widget.pages.isNotEmpty) {
_history.addAll(
widget.pages.map((Page<dynamic> page) => _RouteEntry(
page.createRoute(context),
initialState: _RouteLifecycle.add,
))
int get _nextPagelessRestorationScopeId => _rawNextPagelessRestorationScopeId.value++;
@override
void restoreState(RestorationBucket? oldBucket, bool initialRestore) {
registerForRestoration(_rawNextPagelessRestorationScopeId, 'id');
registerForRestoration(_serializableHistory, 'history');
// Delete everything in the old history and clear the overlay.
while (_history.isNotEmpty) {
_history.removeLast().dispose();
}
assert(_history.isEmpty);
_overlayKey = GlobalKey<OverlayState>();
// Populate the new history from restoration data.
_history.addAll(_serializableHistory.restoreEntriesForPage(null, this));
for (final Page<dynamic> page in widget.pages) {
final _RouteEntry entry = _RouteEntry(
page.createRoute(context),
initialState: _RouteLifecycle.add,
);
} else {
// If there is no page provided, we will need to provide default route
// to initialize the navigator.
initialRoute = initialRoute ?? Navigator.defaultRouteName;
}
if (initialRoute != null) {
_history.addAll(
widget.onGenerateInitialRoutes(
this,
widget.initialRoute ?? Navigator.defaultRouteName
).map((Route<dynamic> route) =>
_RouteEntry(
route,
initialState: _RouteLifecycle.add,
),
),
assert(
entry.route.settings == page,
'The settings getter of a page-based Route must return a Page object. '
'Please set the settings to the Page in the Page.createRoute method.'
);
_history.add(entry);
_history.addAll(_serializableHistory.restoreEntriesForPage(entry, this));
}
// If there was nothing to restore, we need to process the initial route.
if (!_serializableHistory.hasData) {
String? initialRoute = widget.initialRoute;
if (widget.pages.isEmpty) {
initialRoute = initialRoute ?? Navigator.defaultRouteName;
}
if (initialRoute != null) {
_history.addAll(
widget.onGenerateInitialRoutes(
this,
widget.initialRoute ?? Navigator.defaultRouteName,
).map((Route<dynamic> route) => _RouteEntry(
route,
initialState: _RouteLifecycle.add,
restorationInformation: route.settings.name != null
? _RestorationInformation.named(
name: route.settings.name!,
arguments: null,
restorationScopeId: _nextPagelessRestorationScopeId,
)
: null,
),
),
);
}
}
assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }());
_flushHistoryUpdates();
assert(() { _debugLocked = false; return true; }());
}
@override
void didToggleBucket(RestorationBucket? oldBucket) {
super.didToggleBucket(oldBucket);
if (bucket != null) {
_serializableHistory.update(_history);
} else {
_serializableHistory.clear();
}
}
@override
String? get restorationId => widget.restorationScopeId;
@override
void didChangeDependencies() {
super.didChangeDependencies();
......@@ -2820,7 +3374,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
}
_updateEffectiveObservers();
}
if (oldWidget.pages != widget.pages) {
if (oldWidget.pages != widget.pages && !restorePending) {
assert(
widget.pages.isNotEmpty,
'To use the Navigator.pages, there must be at least one page in the list.'
......@@ -3282,8 +3836,12 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
overlayEntry.remove();
entry.dispose();
}
if (rearrangeOverlay)
if (rearrangeOverlay) {
overlay?.rearrange(_allRouteOverlayEntries);
}
if (bucket != null) {
_serializableHistory.update(_history);
}
}
void _flushObserverNotifications() {
......@@ -3416,6 +3974,11 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePushNamed], which pushes a route that can be restored
/// during state restoration.
@optionalTypeArgs
Future<T> pushNamed<T extends Object?>(
String routeName, {
......@@ -3424,6 +3987,42 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return push<T>(_routeNamed<T>(routeName, arguments: arguments)!);
}
/// Push a named route onto the navigator.
///
/// {@macro flutter.widgets.navigator.restorablePushNamed}
///
/// {@macro flutter.widgets.navigator.pushNamed}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool snippet}
///
/// Typical usage is as follows:
///
/// ```dart
/// void _openDetails() {
/// navigator.restorablePushNamed('/nyc/1776');
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
String restorablePushNamed<T extends Object?>(
String routeName, {
Object? arguments,
}) {
assert(routeName != null);
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.named(
name: routeName,
arguments: arguments,
restorationScopeId: _nextPagelessRestorationScopeId,
).toRouteEntry(this, initialState: _RouteLifecycle.push);
_pushEntry(entry);
return entry.restorationId!;
}
/// Replace the current route of the navigator by pushing the route named
/// [routeName] and then disposing the previous route once the new route has
/// finished animating in.
......@@ -3442,6 +4041,11 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePushReplacementNamed], which pushes a replacement route that
/// can be restored during state restoration.
@optionalTypeArgs
Future<T> pushReplacementNamed<T extends Object?, TO extends Object?>(
String routeName, {
......@@ -3451,6 +4055,45 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return pushReplacement<T, TO>(_routeNamed<T>(routeName, arguments: arguments)!, result: result);
}
/// Replace the current route of the navigator by pushing the route named
/// [routeName] and then disposing the previous route once the new route has
/// finished animating in.
///
/// {@macro flutter.widgets.navigator.restorablePushReplacementNamed}
///
/// {@macro flutter.widgets.navigator.pushReplacementNamed}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool snippet}
///
/// Typical usage is as follows:
///
/// ```dart
/// void _startCar() {
/// navigator.restorablePushReplacementNamed('/jouett/1781');
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
String restorablePushReplacementNamed<T extends Object?, TO extends Object?>(
String routeName, {
TO? result,
Object? arguments,
}) {
assert(routeName != null);
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.named(
name: routeName,
arguments: arguments,
restorationScopeId: _nextPagelessRestorationScopeId,
).toRouteEntry(this, initialState: _RouteLifecycle.pushReplace);
_pushReplacementEntry(entry, result);
return entry.restorationId!;
}
/// Pop the current route off the navigator and push a named route in its
/// place.
///
......@@ -3468,6 +4111,11 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePopAndPushNamed], which pushes a new route that can be
/// restored during state restoration.
@optionalTypeArgs
Future<T> popAndPushNamed<T extends Object?, TO extends Object?>(
String routeName, {
......@@ -3478,6 +4126,37 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return pushNamed<T>(routeName, arguments: arguments);
}
/// Pop the current route off the navigator and push a named route in its
/// place.
///
/// {@macro flutter.widgets.navigator.restorablePopAndPushNamed}
///
/// {@macro flutter.widgets.navigator.popAndPushNamed}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool snippet}
///
/// Typical usage is as follows:
///
/// ```dart
/// void _end() {
/// navigator.restorablePopAndPushNamed('/nyc/1776');
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
String restorablePopAndPushNamed<T extends Object?, TO extends Object?>(
String routeName, {
TO? result,
Object? arguments,
}) {
pop<TO>(result);
return restorablePushNamed(routeName, arguments: arguments);
}
/// Push the route with the given name onto the navigator, and then remove all
/// the previous routes until the `predicate` returns true.
///
......@@ -3495,6 +4174,11 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePushNamedAndRemoveUntil], which pushes a new route that can
/// be restored during state restoration.
@optionalTypeArgs
Future<T> pushNamedAndRemoveUntil<T extends Object?>(
String newRouteName,
......@@ -3504,6 +4188,44 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return pushAndRemoveUntil<T>(_routeNamed<T>(newRouteName, arguments: arguments)!, predicate);
}
/// Push the route with the given name onto the navigator, and then remove all
/// the previous routes until the `predicate` returns true.
///
/// {@macro flutter.widgets.navigator.restorablePushNamedAndRemoveUntil}
///
/// {@macro flutter.widgets.navigator.pushNamedAndRemoveUntil}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool snippet}
///
/// Typical usage is as follows:
///
/// ```dart
/// void _openCalendar() {
/// navigator.restorablePushNamedAndRemoveUntil('/calendar', ModalRoute.withName('/'));
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
String restorablePushNamedAndRemoveUntil<T extends Object?>(
String newRouteName,
RoutePredicate predicate, {
Object? arguments,
}) {
assert(newRouteName != null);
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.named(
name: newRouteName,
arguments: arguments,
restorationScopeId: _nextPagelessRestorationScopeId,
).toRouteEntry(this, initialState: _RouteLifecycle.push);
_pushEntryAndRemoveUntil(entry, predicate);
return entry.restorationId!;
}
/// Push the given route onto the navigator.
///
/// {@macro flutter.widgets.navigator.push}
......@@ -3518,26 +4240,95 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePush], which pushes a route that can be restored during
/// state restoration.
@optionalTypeArgs
Future<T> push<T extends Object?>(Route<T> route) {
_pushEntry(_RouteEntry(route, initialState: _RouteLifecycle.push));
return route.popped;
}
bool _debugIsStaticCallback(Function callback) {
bool result = false;
assert(() {
// TODO(goderbauer): remove the kIsWeb check when https://github.com/flutter/flutter/issues/33615 is resolved.
result = kIsWeb || ui.PluginUtilities.getCallbackHandle(callback) != null;
return true;
}());
return result;
}
/// Push a new route onto the navigator.
///
/// {@macro flutter.widgets.navigator.restorablePush}
///
/// {@macro flutter.widgets.navigator.push}
///
/// {@macro flutter.widgets.navigator.restorablePush.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// Typical usage is as follows:
///
/// ```dart
/// static Route _myRouteBuilder(BuildContext context, Object arguments) {
/// return MaterialPageRoute(
/// builder: (BuildContext context) => MyStatefulWidget(),
/// );
/// }
///
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: const Text('Sample Code'),
/// ),
/// floatingActionButton: FloatingActionButton(
/// onPressed: () => Navigator.of(context).restorablePush(_myRouteBuilder),
/// tooltip: 'Increment Counter',
/// child: const Icon(Icons.add),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
Future<T> push<T>(Route<T> route) {
String restorablePush<T extends Object?>(RestorableRouteBuilder<T> routeBuilder, {Object? arguments}) {
assert(routeBuilder != null);
assert(_debugIsStaticCallback(routeBuilder), 'The provided routeBuilder must be a static function.');
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.anonymous(
routeBuilder: routeBuilder,
arguments: arguments,
restorationScopeId: _nextPagelessRestorationScopeId,
).toRouteEntry(this, initialState: _RouteLifecycle.push);
_pushEntry(entry);
return entry.restorationId!;
}
void _pushEntry(_RouteEntry entry) {
assert(!_debugLocked);
assert(() {
_debugLocked = true;
return true;
}());
assert(route != null);
assert(route._navigator == null);
_history.add(_RouteEntry(route, initialState: _RouteLifecycle.push));
assert(entry.route != null);
assert(entry.route._navigator == null);
assert(entry.currentState == _RouteLifecycle.push);
_history.add(entry);
_flushHistoryUpdates();
assert(() {
_debugLocked = false;
return true;
}());
_afterNavigation(route);
return route.popped;
_afterNavigation(entry.route);
}
void _afterNavigation<T>(Route<T>? route) {
void _afterNavigation(Route<dynamic>? route) {
if (!kReleaseMode) {
// Among other uses, performance tools use this event to ensure that perf
// stats reflect the time interval since the last navigation event
......@@ -3548,8 +4339,8 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
routeJsonable = <String, dynamic>{};
String description;
if (route is TransitionRoute<T>) {
final TransitionRoute<T> transitionRoute = route;
if (route is TransitionRoute<dynamic>) {
final TransitionRoute<dynamic> transitionRoute = route;
description = transitionRoute.debugLabel;
} else {
description = '$route';
......@@ -3593,26 +4384,91 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// }
/// ```
/// {@end-tool}
///
/// See also:
///
/// * [restorablePushReplacement], which pushes a replacement route that can
/// be restored during state restoration.
@optionalTypeArgs
Future<T> pushReplacement<T extends Object?, TO extends Object?>(Route<T> newRoute, { TO? result }) {
assert(newRoute != null);
assert(newRoute._navigator == null);
_pushReplacementEntry(_RouteEntry(newRoute, initialState: _RouteLifecycle.pushReplace), result);
return newRoute.popped;
}
/// Replace the current route of the navigator by pushing a new route and
/// then disposing the previous route once the new route has finished
/// animating in.
///
/// {@macro flutter.widgets.navigator.restorablePushReplacement}
///
/// {@macro flutter.widgets.navigator.pushReplacement}
///
/// {@macro flutter.widgets.navigator.restorablePush.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// Typical usage is as follows:
///
/// ```dart
/// static Route _myRouteBuilder(BuildContext context, Object arguments) {
/// return MaterialPageRoute(
/// builder: (BuildContext context) => MyStatefulWidget(),
/// );
/// }
///
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: const Text('Sample Code'),
/// ),
/// floatingActionButton: FloatingActionButton(
/// onPressed: () => Navigator.of(context).restorablePushReplacement(
/// _myRouteBuilder,
/// ),
/// tooltip: 'Increment Counter',
/// child: const Icon(Icons.add),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
String restorablePushReplacement<T extends Object?, TO extends Object?>(RestorableRouteBuilder<T> routeBuilder, { TO? result, Object? arguments }) {
assert(routeBuilder != null);
assert(_debugIsStaticCallback(routeBuilder), 'The provided routeBuilder must be a static function.');
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.anonymous(
routeBuilder: routeBuilder,
arguments: arguments,
restorationScopeId: _nextPagelessRestorationScopeId,
).toRouteEntry(this, initialState: _RouteLifecycle.pushReplace);
_pushReplacementEntry(entry, result);
return entry.restorationId!;
}
void _pushReplacementEntry<TO extends Object?>(_RouteEntry entry, TO? result) {
assert(!_debugLocked);
assert(() {
_debugLocked = true;
return true;
}());
assert(newRoute != null);
assert(newRoute._navigator == null);
assert(entry.route != null);
assert(entry.route._navigator == null);
assert(_history.isNotEmpty);
assert(_history.any(_RouteEntry.isPresentPredicate), 'Navigator has no active routes to replace.');
assert(entry.currentState == _RouteLifecycle.pushReplace);
_history.lastWhere(_RouteEntry.isPresentPredicate).complete(result, isReplaced: true);
_history.add(_RouteEntry(newRoute, initialState: _RouteLifecycle.pushReplace));
_history.add(entry);
_flushHistoryUpdates();
assert(() {
_debugLocked = false;
return true;
}());
_afterNavigation(newRoute);
return newRoute.popped;
_afterNavigation(entry.route);
}
/// Push the given route onto the navigator, and then remove all the previous
......@@ -3633,19 +4489,87 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// }
/// ```
/// {@end-tool}
///
///
/// See also:
///
/// * [restorablePushAndRemoveUntil], which pushes a route that can be
/// restored during state restoration.
@optionalTypeArgs
Future<T> pushAndRemoveUntil<T extends Object?>(Route<T> newRoute, RoutePredicate predicate) {
assert(newRoute != null);
assert(newRoute._navigator == null);
assert(newRoute.overlayEntries.isEmpty);
_pushEntryAndRemoveUntil(_RouteEntry(newRoute, initialState: _RouteLifecycle.push), predicate);
return newRoute.popped;
}
/// Push a new route onto the navigator, and then remove all the previous
/// routes until the `predicate` returns true.
///
/// {@macro flutter.widgets.navigator.restorablePushAndRemoveUntil}
///
/// {@macro flutter.widgets.navigator.pushAndRemoveUntil}
///
/// {@macro flutter.widgets.navigator.restorablePush.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
///
/// {@tool dartpad --template=stateful_widget_material}
///
/// Typical usage is as follows:
///
/// ```dart
/// static Route _myRouteBuilder(BuildContext context, Object arguments) {
/// return MaterialPageRoute(
/// builder: (BuildContext context) => MyStatefulWidget(),
/// );
/// }
///
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: const Text('Sample Code'),
/// ),
/// floatingActionButton: FloatingActionButton(
/// onPressed: () => Navigator.of(context).restorablePushAndRemoveUntil(
/// _myRouteBuilder,
/// ModalRoute.withName('/'),
/// ),
/// tooltip: 'Increment Counter',
/// child: const Icon(Icons.add),
/// ),
/// );
/// }
/// ```
/// {@end-tool}
@optionalTypeArgs
String restorablePushAndRemoveUntil<T extends Object?>(RestorableRouteBuilder<T> newRouteBuilder, RoutePredicate predicate, {Object? arguments}) {
assert(newRouteBuilder != null);
assert(_debugIsStaticCallback(newRouteBuilder), 'The provided routeBuilder must be a static function.');
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
final _RouteEntry entry = _RestorationInformation.anonymous(
routeBuilder: newRouteBuilder,
arguments: arguments,
restorationScopeId: _nextPagelessRestorationScopeId,
).toRouteEntry(this, initialState: _RouteLifecycle.push);
_pushEntryAndRemoveUntil(entry, predicate);
return entry.restorationId!;
}
void _pushEntryAndRemoveUntil(_RouteEntry entry, RoutePredicate predicate) {
assert(!_debugLocked);
assert(() {
_debugLocked = true;
return true;
}());
assert(newRoute != null);
assert(newRoute._navigator == null);
assert(newRoute.overlayEntries.isEmpty);
assert(entry.route != null);
assert(entry.route._navigator == null);
assert(entry.route.overlayEntries.isEmpty);
assert(predicate != null);
assert(entry.currentState == _RouteLifecycle.push);
int index = _history.length - 1;
_history.add(_RouteEntry(newRoute, initialState: _RouteLifecycle.push));
_history.add(entry);
while (index >= 0 && !predicate(_history[index].route)) {
if (_history[index].isPresent)
_history[index].remove();
......@@ -3657,8 +4581,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_debugLocked = false;
return true;
}());
_afterNavigation(newRoute);
return newRoute.popped;
_afterNavigation(entry.route);
}
/// Replaces a route on the navigator with a new route.
......@@ -3669,24 +4592,58 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
///
/// * [replaceRouteBelow], which is the same but identifies the route to be
/// removed by reference to the route above it, rather than directly.
/// * [restorableReplace], which adds a replacement route that can be
/// restored during state restoration.
@optionalTypeArgs
void replace<T extends Object?>({ required Route<dynamic> oldRoute, required Route<T> newRoute }) {
assert(!_debugLocked);
assert(oldRoute != null);
assert(oldRoute._navigator == this);
assert(newRoute != null);
if (oldRoute == newRoute)
_replaceEntry(_RouteEntry(newRoute, initialState: _RouteLifecycle.replace), oldRoute);
}
/// Replaces a route on the navigator with a new route.
///
/// {@macro flutter.widgets.navigator.restorableReplace}
///
/// {@macro flutter.widgets.navigator.replace}
///
/// {@macro flutter.widgets.navigator.restorablePush.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
@optionalTypeArgs
String restorableReplace<T extends Object?>({ required Route<dynamic> oldRoute, required RestorableRouteBuilder<T> newRouteBuilder, Object? arguments }) {
assert(oldRoute != null);
assert(oldRoute._navigator == this);
assert(newRouteBuilder != null);
assert(_debugIsStaticCallback(newRouteBuilder), 'The provided routeBuilder must be a static function.');
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
assert(oldRoute != null);
final _RouteEntry entry = _RestorationInformation.anonymous(
routeBuilder: newRouteBuilder,
arguments: arguments,
restorationScopeId: _nextPagelessRestorationScopeId,
).toRouteEntry(this, initialState: _RouteLifecycle.replace);
_replaceEntry(entry, oldRoute);
return entry.restorationId!;
}
void _replaceEntry(_RouteEntry entry, Route<dynamic> oldRoute) {
assert(!_debugLocked);
if (oldRoute == entry.route)
return;
assert(() {
_debugLocked = true;
return true;
}());
assert(oldRoute._navigator == this);
assert(newRoute._navigator == null);
assert(entry.currentState == _RouteLifecycle.replace);
assert(entry.route._navigator == null);
final int index = _history.indexWhere(_RouteEntry.isRoutePredicate(oldRoute));
assert(index >= 0, 'This Navigator does not contain the specified oldRoute.');
assert(_history[index].isPresent, 'The specified oldRoute has already been removed from the Navigator.');
final bool wasCurrent = oldRoute.isCurrent;
_history.insert(index + 1, _RouteEntry(newRoute, initialState: _RouteLifecycle.replace));
_history.insert(index + 1, entry);
_history[index].remove(isReplaced: true);
_flushHistoryUpdates();
assert(() {
......@@ -3694,7 +4651,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return true;
}());
if (wasCurrent)
_afterNavigation(newRoute);
_afterNavigation(entry.route);
}
/// Replaces a route on the navigator with a new route. The route to be
......@@ -3706,25 +4663,58 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
///
/// * [replace], which is the same but identifies the route to be removed
/// directly.
/// * [restorableReplaceRouteBelow], which adds a replacement route that can
/// be restored during state restoration.
@optionalTypeArgs
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._navigator == this);
assert(newRoute != null);
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) {
assert(anchorRoute != null);
assert(anchorRoute._navigator == this);
_replaceEntryBelow(_RouteEntry(newRoute, initialState: _RouteLifecycle.replace), anchorRoute);
}
/// Replaces a route on the navigator with a new route. The route to be
/// replaced is the one below the given `anchorRoute`.
///
/// {@macro flutter.widgets.navigator.restorableReplaceRouteBelow}
///
/// {@macro flutter.widgets.navigator.replaceRouteBelow}
///
/// {@macro flutter.widgets.navigator.restorablePush.arguments}
///
/// {@macro flutter.widgets.navigator.restorablePushNamed.returnValue}
@optionalTypeArgs
String restorableReplaceRouteBelow<T extends Object?>({ required Route<dynamic> anchorRoute, required RestorableRouteBuilder<T> newRouteBuilder, Object? arguments }) {
assert(anchorRoute != null);
assert(anchorRoute._navigator == this);
assert(newRouteBuilder != null);
assert(_debugIsStaticCallback(newRouteBuilder), 'The provided routeBuilder must be a static function.');
assert(debugIsSerializableForRestoration(arguments), 'The arguments object must be serializable via the StandardMessageCodec.');
assert(anchorRoute != null);
final _RouteEntry entry = _RestorationInformation.anonymous(
routeBuilder: newRouteBuilder,
arguments: arguments,
restorationScopeId: _nextPagelessRestorationScopeId,
).toRouteEntry(this, initialState: _RouteLifecycle.replace);
_replaceEntryBelow(entry, anchorRoute);
return entry.restorationId!;
}
void _replaceEntryBelow(_RouteEntry entry, Route<dynamic> anchorRoute) {
assert(!_debugLocked);
assert(() { _debugLocked = true; return true; }());
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.insert(index + 1, entry);
_history[index].remove(isReplaced: true);
_flushHistoryUpdates();
assert(() { _debugLocked = false; return true; }());
......@@ -3839,7 +4829,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
_debugLocked = false;
return true;
}());
_afterNavigation<dynamic>(entry.route);
_afterNavigation(entry.route);
}
/// Calls [pop] repeatedly until the predicate returns true.
......@@ -3893,7 +4883,7 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
return true;
}());
if (wasCurrent)
_afterNavigation<dynamic>(
_afterNavigation(
_history.cast<_RouteEntry?>().lastWhere(
(_RouteEntry? e) => e != null && _RouteEntry.isPresentPredicate(e),
orElse: () => null
......@@ -3962,6 +4952,15 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
assert(() { _debugLocked = wasDebugLocked!; return true; }());
}
@optionalTypeArgs
Route<T>? _getRouteById<T>(String id) {
assert(id != null);
return _history.cast<_RouteEntry?>().firstWhere(
(_RouteEntry? entry) => entry!.restorationId == id,
orElse: () => null,
)?.route as Route<T>;
}
int get _userGesturesInProgress => _userGesturesInProgressCount;
int _userGesturesInProgressCount = 0;
set _userGesturesInProgress(int value) {
......@@ -3981,7 +4980,6 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
/// Notifies its listeners if the value of [userGestureInProgress] changes.
final ValueNotifier<bool> userGestureInProgressNotifier = ValueNotifier<bool>(false);
/// The navigator is being controlled by a user gesture.
///
/// For example, called when the user beings an iOS back gesture.
......@@ -4064,9 +5062,12 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
child: FocusScope(
node: focusScopeNode,
autofocus: true,
child: Overlay(
key: _overlayKey,
initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
child: UnmanagedRestorationScope(
bucket: bucket,
child: Overlay(
key: _overlayKey,
initialEntries: overlay == null ? _allRouteOverlayEntries.toList(growable: false) : const <OverlayEntry>[],
),
),
),
),
......@@ -4074,3 +5075,646 @@ class NavigatorState extends State<Navigator> with TickerProviderStateMixin {
);
}
}
enum _RouteRestorationType {
named,
anonymous,
}
abstract class _RestorationInformation {
_RestorationInformation(this.type) : assert(type != null);
factory _RestorationInformation.named({
required String name,
required Object? arguments,
required int restorationScopeId,
}) = _NamedRestorationInformation;
factory _RestorationInformation.anonymous({
required RestorableRouteBuilder routeBuilder,
required Object? arguments,
required int restorationScopeId,
}) = _AnonymousRestorationInformation;
factory _RestorationInformation.fromSerializableData(Object data) {
assert(data != null);
final List<Object> casted = data as List<Object>;
assert(casted.isNotEmpty);
final _RouteRestorationType type = _RouteRestorationType.values[casted[0] as int];
switch (type) {
case _RouteRestorationType.named:
return _NamedRestorationInformation.fromSerializableData(casted.sublist(1));
case _RouteRestorationType.anonymous:
return _AnonymousRestorationInformation.fromSerializableData(casted.sublist(1));
}
throw StateError('Invalid type: $type'); // ignore: dead_code
}
final _RouteRestorationType type;
int get restorationScopeId;
Object? _serializableData;
bool get isRestorable => true;
Object getSerializableData() {
_serializableData ??= computeSerializableData();
return _serializableData!;
}
@mustCallSuper
List<Object> computeSerializableData() {
return <Object>[type.index];
}
@protected
Route<dynamic> createRoute(NavigatorState navigator);
_RouteEntry toRouteEntry(NavigatorState navigator, {_RouteLifecycle initialState = _RouteLifecycle.add}) {
assert(navigator != null);
assert(initialState != null);
final Route<Object?> route = createRoute(navigator);
assert(route != null);
return _RouteEntry(
route,
initialState: initialState,
restorationInformation: this,
);
}
}
class _NamedRestorationInformation extends _RestorationInformation {
_NamedRestorationInformation({
required this.name,
required this.arguments,
required this.restorationScopeId,
}) : assert(name != null), super(_RouteRestorationType.named);
factory _NamedRestorationInformation.fromSerializableData(List<Object> data) {
assert(data.length >= 2);
return _NamedRestorationInformation(
restorationScopeId: data[0] as int,
name: data[1] as String,
arguments: data.length > 2 ? data[2] : null,
);
}
@override
List<Object> computeSerializableData() {
return super.computeSerializableData()..addAll(<Object>[
restorationScopeId,
name,
if (arguments != null)
arguments!,
]);
}
@override
final int restorationScopeId;
final String name;
final Object? arguments;
@override
Route<dynamic> createRoute(NavigatorState navigator) {
final Route<dynamic> route = navigator._routeNamed<dynamic>(name, arguments: arguments, allowNull: false)!;
assert(route != null);
return route;
}
}
class _AnonymousRestorationInformation extends _RestorationInformation {
_AnonymousRestorationInformation({
required this.routeBuilder,
required this.arguments,
required this.restorationScopeId,
}) : assert(routeBuilder != null), super(_RouteRestorationType.anonymous);
factory _AnonymousRestorationInformation.fromSerializableData(List<Object> data) {
assert(data.length > 1);
final RestorableRouteBuilder routeBuilder = ui.PluginUtilities.getCallbackFromHandle(ui.CallbackHandle.fromRawHandle(data[1] as int)) as RestorableRouteBuilder;
assert(routeBuilder != null);
return _AnonymousRestorationInformation(
restorationScopeId: data[0] as int,
routeBuilder: routeBuilder,
arguments: data.length > 2 ? data[2] : null,
);
}
@override
// TODO(goderbauer): remove the kIsWeb check when https://github.com/flutter/flutter/issues/33615 is resolved.
bool get isRestorable => !kIsWeb;
@override
List<Object> computeSerializableData() {
assert(isRestorable);
final ui.CallbackHandle? handle = ui.PluginUtilities.getCallbackHandle(routeBuilder);
assert(handle != null);
return super.computeSerializableData()..addAll(<Object>[
restorationScopeId,
handle!.toRawHandle(),
if (arguments != null)
arguments!,
]);
}
@override
final int restorationScopeId;
final RestorableRouteBuilder routeBuilder;
final Object? arguments;
@override
Route<dynamic> createRoute(NavigatorState navigator) {
final Route<dynamic> result = routeBuilder(navigator.context, arguments);
assert(result != null);
return result;
}
}
class _HistoryProperty extends RestorableProperty<Map<String?, List<Object>>?> {
// Routes not associated with a page are stored under key 'null'.
Map<String?, List<Object>>? _pageToPagelessRoutes;
// Updating.
void update(List<_RouteEntry> history) {
assert(isRegistered);
final bool wasUninitialized = _pageToPagelessRoutes == null;
bool needsSerialization = wasUninitialized;
_pageToPagelessRoutes ??= <String, List<Object>>{};
_RouteEntry? currentPage;
List<Object> newRoutesForCurrentPage = <Object>[];
List<Object> oldRoutesForCurrentPage = _pageToPagelessRoutes![null] ?? const <Object>[];
bool restorationEnabled = true;
final Map<String?, List<Object>> newMap = <String?, List<Object>>{};
final Set<String?> removedPages = _pageToPagelessRoutes!.keys.toSet();
for (final _RouteEntry entry in history) {
if (!entry.isPresentForRestoration) {
entry.restorationEnabled = false;
continue;
}
assert(entry.isPresentForRestoration);
if (entry.hasPage) {
needsSerialization = needsSerialization || newRoutesForCurrentPage.length != oldRoutesForCurrentPage.length;
_finalizePage(newRoutesForCurrentPage, currentPage, newMap, removedPages);
currentPage = entry;
restorationEnabled = entry.restorationId != null;
entry.restorationEnabled = restorationEnabled;
if (restorationEnabled) {
assert(entry.restorationId != null);
newRoutesForCurrentPage = <Object>[];
oldRoutesForCurrentPage = _pageToPagelessRoutes![entry.restorationId] ?? const <Object>[];
} else {
newRoutesForCurrentPage = const <Object>[];
oldRoutesForCurrentPage = const <Object>[];
}
continue;
}
assert(!entry.hasPage);
restorationEnabled = restorationEnabled && entry.restorationInformation?.isRestorable == true;
entry.restorationEnabled = restorationEnabled;
if (restorationEnabled) {
assert(entry.restorationId != null);
assert(currentPage == null || currentPage.restorationId != null);
assert(entry.restorationInformation != null);
final Object serializedData = entry.restorationInformation!.getSerializableData();
needsSerialization = needsSerialization
|| oldRoutesForCurrentPage.length <= newRoutesForCurrentPage.length
|| oldRoutesForCurrentPage[newRoutesForCurrentPage.length] != serializedData;
newRoutesForCurrentPage.add(serializedData);
}
}
needsSerialization = needsSerialization || newRoutesForCurrentPage.length != oldRoutesForCurrentPage.length;
_finalizePage(newRoutesForCurrentPage, currentPage, newMap, removedPages);
needsSerialization = needsSerialization || removedPages.isNotEmpty;
assert(wasUninitialized || _debugMapsEqual(_pageToPagelessRoutes!, newMap) != needsSerialization);
if (needsSerialization) {
_pageToPagelessRoutes = newMap;
notifyListeners();
}
}
void _finalizePage(
List<Object> routes,
_RouteEntry? page,
Map<String?, List<Object>> pageToRoutes,
Set<String?> pagesToRemove,
) {
assert(page == null || page.hasPage);
assert(pageToRoutes != null);
assert(!pageToRoutes.containsKey(page?.restorationId));
if (routes != null && routes.isNotEmpty) {
assert(page == null || page.restorationId != null);
final String? restorationId = page?.restorationId;
pageToRoutes[restorationId] = routes;
pagesToRemove.remove(restorationId);
}
}
bool _debugMapsEqual(Map<String?, List<Object>> a, Map<String?, List<Object>> b) {
if (!setEquals(a.keys.toSet(), b.keys.toSet())) {
return false;
}
for (final String? key in a.keys) {
if (!listEquals(a[key], b[key])) {
return false;
}
}
return true;
}
void clear() {
assert(isRegistered);
if (_pageToPagelessRoutes == null) {
return;
}
_pageToPagelessRoutes = null;
notifyListeners();
}
// Restoration.
bool get hasData => _pageToPagelessRoutes != null;
List<_RouteEntry> restoreEntriesForPage(_RouteEntry? page, NavigatorState navigator) {
assert(isRegistered);
assert(page == null || page.hasPage);
final List<_RouteEntry> result = <_RouteEntry>[];
if (_pageToPagelessRoutes == null || (page != null && page.restorationId == null)) {
return result;
}
final List<Object>? serializedData = _pageToPagelessRoutes![page?.restorationId];
if (serializedData == null) {
return result;
}
for (final Object data in serializedData) {
result.add(_RestorationInformation.fromSerializableData(data).toRouteEntry(navigator));
}
return result;
}
// RestorableProperty overrides.
@override
Map<String?, List<Object>>? createDefaultValue() {
return null;
}
@override
Map<String?, List<Object>>? fromPrimitives(Object data) {
final Map<dynamic, dynamic> casted = data as Map<dynamic, dynamic>;
return casted.map<String, List<Object>>((dynamic key, dynamic value) => MapEntry<String, List<Object>>(
key as String,
List<Object>.from(value as List<dynamic>, growable: true),
));
}
@override
void initWithValue(Map<String?, List<Object>>? value) {
_pageToPagelessRoutes = value;
}
@override
Object? toPrimitives() {
return _pageToPagelessRoutes;
}
@override
bool get enabled => hasData;
}
/// A callback that given a [BuildContext] finds a [NavigatorState].
///
/// Used by [RestorableRouteFuture.navigatorFinder] to determine the navigator
/// to which a new route should be added.
typedef NavigatorFinderCallback = NavigatorState Function(BuildContext context);
/// A callback that given some `arguments` and a `navigator` adds a new
/// restorable route to that `navigator` and resturns the opaque ID of that
/// new route.
///
/// Usually, this callback calls one of the imperative methods on the Navigator
/// that have "restorable" in the name and returns their return value.
///
/// Used by [RestorableRouteFuture.onPresent].
typedef RoutePresentationCallback = String Function(NavigatorState navigator, Object? arguments);
/// A callback to handle the result of a completed [Route].
///
/// The return value of the route (which can be null for e.g. void routes) is
/// passed to the callback.
///
/// Used by [RestorableRouteFuture.onComplete].
typedef RouteCompletionCallback<T> = void Function(T result);
/// Gives access to a [Route] object and its return value that was added to a
/// navigator via one of its "restorable" API methods.
///
/// When a [State] object wants access to the return value of a [Route] object
/// it has pushed onto the [Navigator], a [RestorableRouteFuture] ensures that
/// it will also have access to that value after state restoration.
///
/// To show a new route on the navigator defined by the [navigatorFinder], call
/// [present], which will invoke the [onPresent] callback. The [onPresent]
/// callback must add a new route to the navigator provided to it using one
/// of the "restorable" API methods. When the newly added route completes, the
/// [onComplete] callback executes. It is given the return value of the route,
/// which may be null.
///
/// While the route added via [present] is shown on the navigator, it can be
/// accessed via the [route] getter.
///
/// If the property is restored to a state in which [present] had been called on
/// it, but the route has not completed yet, the [RestorableRouteFuture] will
/// obtain the restored route object from the navigator again and call
/// [onComplete] once it completes.
///
/// The [RestorableRouteFuture] can only keep track of one active [route].
/// When [present] has been called to add a route, it may only be called again
/// after the previously added route has completed.
///
/// {@tool dartpad --template=freeform}
/// This example uses a [RestorableRouteFuture] in the `_MyHomeState` to push a
/// new `MyCounter` route and to retrieve its return value.
///
/// ```dart imports
/// import 'package:flutter/material.dart';
/// ```
///
/// ```dart main
/// void main() => runApp(MyApp());
/// ```
///
/// ```dart preamble
/// class MyApp extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// restorationScopeId: 'app',
/// home: Scaffold(
/// appBar: AppBar(title: Text('RestorableRouteFuture Example')),
/// body: MyHome(),
/// ),
/// );
/// }
/// }
/// ```
///
/// ```dart
/// class MyHome extends StatefulWidget {
/// const MyHome({Key key}) : super(key: key);
///
/// @override
/// State<MyHome> createState() => _MyHomeState();
/// }
///
/// class _MyHomeState extends State<MyHome> with RestorationMixin {
/// final RestorableInt _lastCount = RestorableInt(0);
/// RestorableRouteFuture<int> _counterRoute;
///
/// @override
/// String get restorationId => 'home';
///
/// void initState() {
/// super.initState();
/// _counterRoute = RestorableRouteFuture<int>(
/// onPresent: (NavigatorState navigator, Object arguments) {
/// // Defines what route should be shown (and how it should be added
/// // to the navigator) when `RestorableRouteFuture.present` is called.
/// return navigator.restorablePush(
/// _counterRouteBuilder,
/// arguments: arguments,
/// );
/// },
/// onComplete: (int count) {
/// // Defines what should happen with the return value when the route
/// // completes.
/// setState(() {
/// _lastCount.value = count;
/// });
/// }
/// );
/// }
///
/// @override
/// void restoreState(RestorationBucket oldBucket, bool initialRestore) {
/// // Register the `RestorableRouteFuture` with the state restoration framework.
/// registerForRestoration(_counterRoute, 'route');
/// registerForRestoration(_lastCount, 'count');
/// }
///
/// @override
/// void dispose() {
/// super.dispose();
/// _lastCount.dispose();
/// _counterRoute?.dispose();
/// }
///
/// // A static `RestorableRouteBuilder` that can re-create the route during
/// // state restoration.
/// static Route<int> _counterRouteBuilder(BuildContext context, Object arguments) {
/// return MaterialPageRoute(
/// builder: (BuildContext context) => MyCounter(
/// title: arguments as String,
/// ),
/// );
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Center(
/// child: Column(
/// mainAxisSize: MainAxisSize.min,
/// children: <Widget>[
/// Text('Last count: ${_lastCount.value}'),
/// RaisedButton(
/// onPressed: () {
/// // Show the route defined by the `RestorableRouteFuture`.
/// _counterRoute.present('Awesome Counter');
/// },
/// child: Text('Open Counter'),
/// ),
/// ],
/// ),
/// );
/// }
/// }
///
/// // Widget for the route pushed by the `RestorableRouteFuture`.
/// class MyCounter extends StatefulWidget {
/// const MyCounter({Key key, this.title}) : super(key: key);
///
/// final String title;
///
/// @override
/// State<MyCounter> createState() => _MyCounterState();
/// }
///
/// class _MyCounterState extends State<MyCounter> with RestorationMixin {
/// final RestorableInt _count = RestorableInt(0);
///
/// @override
/// String get restorationId => 'counter';
///
/// @override
/// void restoreState(RestorationBucket oldBucket, bool initialRestore) {
/// registerForRestoration(_count, 'count');
/// }
///
/// @override
/// void dispose() {
/// super.dispose();
/// _count.dispose();
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// appBar: AppBar(
/// title: Text(widget.title),
/// leading: BackButton(
/// onPressed: () {
/// // Return the current count of the counter from this route.
/// Navigator.of(context).pop(_count.value);
/// },
/// ),
/// ),
/// body: Center(
/// child: Text('Count: ${_count.value}'),
/// ),
/// floatingActionButton: FloatingActionButton(
/// child: Icon(Icons.add),
/// onPressed: () {
/// setState(() {
/// _count.value++;
/// });
/// },
/// ),
/// );
/// }
/// }
/// ```
/// {@end-tool}
class RestorableRouteFuture<T> extends RestorableProperty<String?> {
/// Creates a [RestorableRouteFuture].
///
/// The [onPresent] and [navigatorFinder] arguments must not be null.
RestorableRouteFuture({
this.navigatorFinder = _defaultNavigatorFinder,
required this.onPresent,
this.onComplete,
}) : assert(onPresent != null), assert(navigatorFinder != null);
/// A callback that given the [BuildContext] of the [State] object to which
/// this property is registered returns the [NavigatorState] of the navigator
/// to which the route instantiated in [onPresent] is added.
final NavigatorFinderCallback navigatorFinder;
/// A callback that add a new [Route] to the provided navigator.
///
/// The callback must use one of the API methods on the [NavigatorState] that
/// have "restorable" in their name (e.g. [NavigatorState.restorablePush],
/// [NavigatorState.restorablePushNamed], etc.) and return the opaque ID
/// returned by those methods.
///
/// This callback is invoked when [present] is called with the `arguments`
/// Object that was passed to that method and the [NavigatorState] obtained
/// from [navigatorFinder].
final RoutePresentationCallback onPresent;
/// A callback that is invoked when the [Route] added via [onPresent]
/// completes.
///
/// The return value of that route is passed to this method.
final RouteCompletionCallback<T>? onComplete;
/// Shows the route created by [onPresent] and invoke [onComplete] when it
/// completes.
///
/// The `arguments` object is passed to [onPresent] and can be used to
/// customize the route. It must be serializable via the
/// [StandardMessageCodec]. Often, a [Map] is used to pass key-value pairs.
void present([Object? arguments]) {
assert(!isPresent);
assert(isRegistered);
final String routeId = onPresent(_navigator, arguments);
assert(routeId != null);
_hookOntoRouteFuture(routeId);
notifyListeners();
}
/// Whether the [Route] created by [present] is currently shown.
///
/// Returns true after [present] has been called until the [Route] compeltes.
bool get isPresent => route != null;
/// The route that [present] added to the Navigator.
///
/// Returns null when currently no route is shown
Route<T>? get route => _route;
Route<T>? _route;
@override
String? createDefaultValue() => null;
@override
void initWithValue(String? value) {
if (value != null) {
_hookOntoRouteFuture(value);
}
}
@override
Object? toPrimitives() {
assert(route != null);
assert(enabled);
return route?.restorationScopeId.value;
}
@override
String fromPrimitives(Object data) {
assert(data != null);
return data as String;
}
bool _disposed = false;
@override
void dispose() {
super.dispose();
_route?.restorationScopeId.removeListener(notifyListeners);
_disposed = true;
}
@override
bool get enabled => route?.restorationScopeId.value != null;
NavigatorState get _navigator {
final NavigatorState navigator = navigatorFinder(state.context);
assert(navigator != null);
return navigator;
}
void _hookOntoRouteFuture(String id) {
assert(id != null);
_route = _navigator._getRouteById<T>(id);
assert(_route != null);
route!.restorationScopeId.addListener(notifyListeners);
route!.popped.then((dynamic result) {
if (_disposed) {
return;
}
_route?.restorationScopeId.removeListener(notifyListeners);
_route = null;
notifyListeners();
if (onComplete != null) {
onComplete!(result as T);
}
});
}
static NavigatorState _defaultNavigatorFinder(BuildContext context) => Navigator.of(context, nullOk: false)!;
}
......@@ -499,10 +499,13 @@ abstract class RestorableProperty<T> extends ChangeNotifier {
}
/// The [State] object that this property is registered with.
///
/// Must only be called when [isRegistered] is true.
@protected
State? get state {
State get state {
assert(isRegistered);
assert(_debugAssertNotDisposed());
return _owner;
return _owner!;
}
/// Whether this property is currently registered with a [RestorationMixin].
......@@ -609,13 +612,10 @@ abstract class RestorableProperty<T> extends ChangeNotifier {
/// class RestorationExampleApp extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// // The [RootRestorationScope] can be removed once it is part of [MaterialApp].
/// return RootRestorationScope(
/// restorationId: 'root',
/// child: MaterialApp(
/// title: 'Restorable Counter',
/// home: RestorableCounter(restorationId: 'counter'),
/// ),
/// return MaterialApp(
/// restorationScopeId: 'app',
/// title: 'Restorable Counter',
/// home: RestorableCounter(restorationId: 'counter'),
/// );
/// }
/// }
......
......@@ -18,6 +18,7 @@ import 'modal_barrier.dart';
import 'navigator.dart';
import 'overlay.dart';
import 'page_storage.dart';
import 'restoration.dart';
import 'transitions.dart';
// Examples can assume:
......@@ -773,54 +774,64 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
@override
Widget build(BuildContext context) {
return _ModalScopeStatus(
route: widget.route,
isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
canPop: widget.route.canPop, // _routeSetState is called if this updates
child: Offstage(
offstage: widget.route.offstage, // _routeSetState is called if this updates
child: PageStorage(
bucket: widget.route._storageBucket, // immutable
child: Actions(
actions: _actionMap,
child: FocusScope(
node: focusScopeNode, // immutable
child: RepaintBoundary(
child: AnimatedBuilder(
animation: _listenable, // immutable
builder: (BuildContext context, Widget? child) {
return widget.route.buildTransitions(
context,
widget.route.animation!,
widget.route.secondaryAnimation!,
// This additional AnimatedBuilder is include because if the
// value of the userGestureInProgressNotifier changes, it's
// only necessary to rebuild the IgnorePointer widget and set
// the focus node's ability to focus.
AnimatedBuilder(
animation: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier<bool>(false),
builder: (BuildContext context, Widget? child) {
final bool ignoreEvents = _shouldIgnoreFocusRequest;
focusScopeNode.canRequestFocus = !ignoreEvents;
return IgnorePointer(
ignoring: ignoreEvents,
child: child,
return AnimatedBuilder(
animation: widget.route.restorationScopeId,
builder: (BuildContext context, Widget? child) {
assert(child != null);
return RestorationScope(
restorationId: widget.route.restorationScopeId.value,
child: child!,
);
},
child: _ModalScopeStatus(
route: widget.route,
isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
canPop: widget.route.canPop, // _routeSetState is called if this updates
child: Offstage(
offstage: widget.route.offstage, // _routeSetState is called if this updates
child: PageStorage(
bucket: widget.route._storageBucket, // immutable
child: Actions(
actions: _actionMap,
child: FocusScope(
node: focusScopeNode, // immutable
child: RepaintBoundary(
child: AnimatedBuilder(
animation: _listenable, // immutable
builder: (BuildContext context, Widget? child) {
return widget.route.buildTransitions(
context,
widget.route.animation!,
widget.route.secondaryAnimation!,
// This additional AnimatedBuilder is include because if the
// value of the userGestureInProgressNotifier changes, it's
// only necessary to rebuild the IgnorePointer widget and set
// the focus node's ability to focus.
AnimatedBuilder(
animation: widget.route.navigator?.userGestureInProgressNotifier ?? ValueNotifier<bool>(false),
builder: (BuildContext context, Widget? child) {
final bool ignoreEvents = _shouldIgnoreFocusRequest;
focusScopeNode.canRequestFocus = !ignoreEvents;
return IgnorePointer(
ignoring: ignoreEvents,
child: child,
);
},
child: child,
),
);
},
child: _page ??= RepaintBoundary(
key: widget.route._subtreeKey, // immutable
child: Builder(
builder: (BuildContext context) {
return widget.route.buildPage(
context,
widget.route.animation!,
widget.route.secondaryAnimation!,
);
},
child: child,
),
);
},
child: _page ??= RepaintBoundary(
key: widget.route._subtreeKey, // immutable
child: Builder(
builder: (BuildContext context) {
return widget.route.buildPage(
context,
widget.route.animation!,
widget.route.secondaryAnimation!,
);
},
),
),
),
......
......@@ -97,6 +97,10 @@ void main() {
expect(tester.takeException(), isFlutterError);
expect(unknownForRouteCalled, '/');
// Work-around for https://github.com/flutter/flutter/issues/65655.
await tester.pumpWidget(Container());
expect(tester.takeException(), isAssertionError);
});
testWidgets('Can use navigatorKey to navigate', (WidgetTester tester) async {
......
......@@ -16,9 +16,9 @@ const String alternativeText = 'Everything is awesome!!';
void main() {
testWidgets('CupertinoTextField restoration', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
child: TestWidget(),
restorationId: 'root',
const CupertinoApp(
restorationScopeId: 'app',
home: TestWidget(),
),
);
......@@ -27,11 +27,11 @@ void main() {
testWidgets('CupertinoTextField restoration with external controller', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
child: TestWidget(
const CupertinoApp(
restorationScopeId: 'app',
home: TestWidget(
useExternal: true,
),
restorationId: 'root',
),
);
......@@ -102,17 +102,15 @@ class TestWidgetState extends State<TestWidget> with RestorationMixin {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Material(
child: Align(
alignment: Alignment.center,
child: SizedBox(
width: 50,
child: CupertinoTextField(
restorationId: 'text',
maxLines: 3,
controller: widget.useExternal ? controller.value : null,
),
return Material(
child: Align(
alignment: Alignment.center,
child: SizedBox(
width: 50,
child: CupertinoTextField(
restorationId: 'text',
maxLines: 3,
controller: widget.useExternal ? controller.value : null,
),
),
),
......
......@@ -384,6 +384,10 @@ void main() {
);
expect(tester.takeException(), isFlutterError);
expect(log, <String>['onGenerateRoute /', 'onUnknownRoute /']);
// Work-around for https://github.com/flutter/flutter/issues/65655.
await tester.pumpWidget(Container());
expect(tester.takeException(), isAssertionError);
});
testWidgets('MaterialApp with builder and no route information works.', (WidgetTester tester) async {
......
......@@ -142,6 +142,9 @@ void main() {
' PageStorage\n'
' Offstage\n'
' _ModalScopeStatus\n'
' UnmanagedRestorationScope\n'
' RestorationScope\n'
' AnimatedBuilder\n'
' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#00000]\n'
' Semantics\n'
' _EffectiveTickerMode\n'
......@@ -149,6 +152,7 @@ void main() {
' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#00000]\n'
' _Theatre\n'
' Overlay-[LabeledGlobalKey<OverlayState>#00000]\n'
' UnmanagedRestorationScope\n'
' _FocusMarker\n'
' Semantics\n'
' FocusScope\n'
......@@ -187,6 +191,10 @@ void main() {
' _FocusMarker\n'
' Focus\n'
' Shortcuts\n'
' UnmanagedRestorationScope\n'
' RestorationScope\n'
' UnmanagedRestorationScope\n'
' RootRestorationScope\n'
' WidgetsApp-[GlobalObjectKey _MaterialAppState#00000]\n'
' HeroControllerScope\n'
' ScrollConfiguration\n'
......
......@@ -15,9 +15,9 @@ const String alternativeText = 'Everything is awesome!!';
void main() {
testWidgets('TextField restoration', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
child: TestWidget(),
restorationId: 'root',
const MaterialApp(
restorationScopeId: 'app',
home: TestWidget(),
),
);
......@@ -26,11 +26,11 @@ void main() {
testWidgets('TextField restoration with external controller', (WidgetTester tester) async {
await tester.pumpWidget(
const RootRestorationScope(
child: TestWidget(
const MaterialApp(
restorationScopeId: 'root',
home: TestWidget(
useExternal: true,
),
restorationId: 'root',
),
);
......@@ -101,17 +101,15 @@ class TestWidgetState extends State<TestWidget> with RestorationMixin {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Material(
child: Align(
alignment: Alignment.center,
child: SizedBox(
width: 50,
child: TextField(
restorationId: 'text',
maxLines: 3,
controller: widget.useExternal ? controller.value : null,
),
return Material(
child: Align(
alignment: Alignment.center,
child: SizedBox(
width: 50,
child: TextField(
restorationId: 'text',
maxLines: 3,
controller: widget.useExternal ? controller.value : null,
),
),
),
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.8
import 'dart:ui';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/material.dart';
void main() {
testWidgets('Restoration Smoke Test', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home', count: 0), findsOneWidget);
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 1), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('home', count: 1), findsOneWidget);
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 2), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 3), findsOneWidget);
await tester.restoreFrom(data);
expect(findRoute('home', count: 2), findsOneWidget);
});
testWidgets('restorablePushNamed', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo', arguments: 3);
await tester.pumpAndSettle();
expect(findRoute('home'), findsNothing);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
expect(findRoute('Foo', count: 0, arguments: 3), findsOneWidget);
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('home'), findsNothing);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 2, arguments: 3), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar', arguments: 4);
await tester.pumpAndSettle();
expect(findRoute('Bar', arguments: 4), findsOneWidget);
await tester.restoreFrom(data);
expect(findRoute('home'), findsNothing);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
expect(findRoute('Bar'), findsNothing);
});
testWidgets('restorablePushReplacementNamed', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacementNamed('Foo', arguments: 3);
await tester.pumpAndSettle();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 0, arguments: 3), findsOneWidget);
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 2, arguments: 3), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar', arguments: 4);
await tester.pumpAndSettle();
expect(findRoute('Bar', arguments: 4), findsOneWidget);
await tester.restoreFrom(data);
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
expect(findRoute('Bar'), findsNothing);
});
testWidgets('restorablePopAndPushNamed', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePopAndPushNamed('Foo', arguments: 3);
await tester.pumpAndSettle();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 0, arguments: 3), findsOneWidget);
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 2, arguments: 3), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar', arguments: 4);
await tester.pumpAndSettle();
expect(findRoute('Bar', arguments: 4), findsOneWidget);
await tester.restoreFrom(data);
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
expect(findRoute('Bar'), findsNothing);
});
testWidgets('restorablePushNamedAndRemoveUntil', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamedAndRemoveUntil('Foo', (Route<dynamic> _) => false, arguments: 3);
await tester.pumpAndSettle();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 0, arguments: 3), findsOneWidget);
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 2, arguments: 3), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar', arguments: 4);
await tester.pumpAndSettle();
expect(findRoute('Bar', arguments: 4), findsOneWidget);
await tester.restoreFrom(data);
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1, arguments: 3), findsOneWidget);
expect(findRoute('Bar'), findsNothing);
});
testWidgets('restorablePush', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeBuilder, arguments: 'Foo');
await tester.pumpAndSettle();
expect(findRoute('home'), findsNothing);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
expect(findRoute('Foo', count: 0), findsOneWidget);
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 1), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('home'), findsNothing);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
expect(findRoute('Foo', count: 1), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 2), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
await tester.pumpAndSettle();
expect(findRoute('Bar'), findsOneWidget);
await tester.restoreFrom(data);
expect(findRoute('home'), findsNothing);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
expect(findRoute('Foo', count: 1), findsOneWidget);
expect(findRoute('Bar'), findsNothing);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
testWidgets('restorablePush adds route on all platforms', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeBuilder, arguments: 'Foo');
await tester.pumpAndSettle();
expect(findRoute('Foo'), findsOneWidget);
});
testWidgets('restorablePushReplacement', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home', count: 0), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacement(_routeBuilder, arguments: 'Foo');
await tester.pumpAndSettle();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 0), findsOneWidget);
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 1), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 2), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
await tester.pumpAndSettle();
expect(findRoute('Bar'), findsOneWidget);
await tester.restoreFrom(data);
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1), findsOneWidget);
expect(findRoute('Bar'), findsNothing);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
testWidgets('restorablePushReplacement adds route on all platforms', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacement(_routeBuilder, arguments: 'Foo');
await tester.pumpAndSettle();
expect(findRoute('Foo'), findsOneWidget);
});
testWidgets('restorablePushAndRemoveUntil', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home', count: 0), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushAndRemoveUntil(_routeBuilder, (Route<dynamic> _) => false, arguments: 'Foo');
await tester.pumpAndSettle();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 0), findsOneWidget);
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 1), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 2), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
await tester.pumpAndSettle();
expect(findRoute('Bar'), findsOneWidget);
await tester.restoreFrom(data);
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1), findsOneWidget);
expect(findRoute('Bar'), findsNothing);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
testWidgets('restorablePushAndRemoveUntil adds route on all platforms', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushAndRemoveUntil(_routeBuilder, (Route<dynamic> _) => false, arguments: 'Foo');
await tester.pumpAndSettle();
expect(findRoute('Foo'), findsOneWidget);
});
testWidgets('restorableReplace', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home', count: 0), findsOneWidget);
final Route<Object> oldRoute = ModalRoute.of(tester.element(find.text('Route: home')));
expect(oldRoute.settings.name, 'home');
tester.state<NavigatorState>(find.byType(Navigator)).restorableReplace(newRouteBuilder: _routeBuilder, arguments: 'Foo', oldRoute: oldRoute);
await tester.pumpAndSettle();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 0), findsOneWidget);
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 1), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 2), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
await tester.pumpAndSettle();
expect(findRoute('Bar'), findsOneWidget);
await tester.restoreFrom(data);
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 1), findsOneWidget);
expect(findRoute('Bar'), findsNothing);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
testWidgets('restorableReplace adds route on all platforms', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home', count: 0), findsOneWidget);
final Route<Object> oldRoute = ModalRoute.of(tester.element(find.text('Route: home')));
expect(oldRoute.settings.name, 'home');
tester.state<NavigatorState>(find.byType(Navigator)).restorableReplace(newRouteBuilder: _routeBuilder, arguments: 'Foo', oldRoute: oldRoute);
await tester.pumpAndSettle();
expect(findRoute('Foo'), findsOneWidget);
});
testWidgets('restorableReplaceRouteBelow', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home', count: 0), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Anchor');
await tester.pumpAndSettle();
await tapRouteCounter('Anchor', tester);
expect(findRoute('home'), findsNothing);
expect(findRoute('home', count: 0, skipOffstage: false), findsOneWidget);
expect(findRoute('Anchor', count: 1), findsOneWidget);
final Route<Object> anchor = ModalRoute.of(tester.element(find.text('Route: Anchor')));
expect(anchor.settings.name, 'Anchor');
tester.state<NavigatorState>(find.byType(Navigator)).restorableReplaceRouteBelow(newRouteBuilder: _routeBuilder, arguments: 'Foo', anchorRoute: anchor);
await tester.pumpAndSettle();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 0, skipOffstage: false), findsOneWidget);
expect(findRoute('Anchor', count: 1), findsOneWidget);
await tapRouteCounter('Anchor', tester);
expect(findRoute('Anchor', count: 2), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 0, skipOffstage: false), findsOneWidget);
expect(findRoute('Anchor', count: 2), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tapRouteCounter('Anchor', tester);
expect(findRoute('Anchor', count: 3), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
await tester.pumpAndSettle();
expect(findRoute('Bar'), findsOneWidget);
await tester.restoreFrom(data);
expect(findRoute('home', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 0, skipOffstage: false), findsOneWidget);
expect(findRoute('Anchor', count: 2), findsOneWidget);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
testWidgets('restorableReplaceRouteBelow adds route on all platforms', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home', count: 0), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Anchor');
await tester.pumpAndSettle();
await tapRouteCounter('Anchor', tester);
expect(findRoute('home'), findsNothing);
expect(findRoute('home', count: 0, skipOffstage: false), findsOneWidget);
expect(findRoute('Anchor', count: 1), findsOneWidget);
final Route<Object> anchor = ModalRoute.of(tester.element(find.text('Route: Anchor')));
expect(anchor.settings.name, 'Anchor');
tester.state<NavigatorState>(find.byType(Navigator)).restorableReplaceRouteBelow(newRouteBuilder: _routeBuilder, arguments: 'Foo', anchorRoute: anchor);
await tester.pumpAndSettle();
expect(findRoute('Foo', skipOffstage: false), findsOneWidget);
});
testWidgets('restoring a popped route', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
await tester.pumpAndSettle();
await tapRouteCounter('Foo', tester);
await tapRouteCounter('Foo', tester);
expect(findRoute('home'), findsNothing);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
expect(findRoute('Foo', count: 2), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 3), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('Foo'), findsNothing);
await tester.restoreFrom(data);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
expect(findRoute('Foo', count: 2), findsOneWidget);
});
testWidgets('popped routes are not restored', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
await tester.pumpAndSettle();
expect(findRoute('Foo'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
await tester.pumpAndSettle();
expect(findRoute('Bar'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('Bar'), findsNothing);
expect(findRoute('Foo'), findsOneWidget);
expect(findRoute('home', skipOffstage: false), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('Bar'), findsNothing);
expect(findRoute('Foo'), findsOneWidget);
expect(findRoute('home', skipOffstage: false), findsOneWidget);
});
testWidgets('routes that are in the process of push are restored', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
await tester.pump();
await tester.pump();
expect(findRoute('Foo'), findsOneWidget);
// Push is in progress.
final ModalRoute<Object> route1 = ModalRoute.of(tester.element(find.text('Route: Foo')));
final String route1id = route1.restorationScopeId.value;
expect(route1id, isNotNull);
expect(route1.settings.name, 'Foo');
expect(route1.animation.isCompleted, isFalse);
expect(route1.animation.isDismissed, isFalse);
expect(route1.isActive, isTrue);
await tester.restartAndRestore();
expect(findRoute('Foo'), findsOneWidget);
expect(findRoute('home', skipOffstage: false), findsOneWidget);
final ModalRoute<Object> route2 = ModalRoute.of(tester.element(find.text('Route: Foo')));
expect(route2, isNot(same(route1)));
expect(route1.restorationScopeId.value, route1id);
expect(route2.animation.isCompleted, isTrue);
expect(route2.isActive, isTrue);
});
testWidgets('routes that are in the process of pop are not restored', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
await tester.pumpAndSettle();
final ModalRoute<Object> route1 = ModalRoute.of(tester.element(find.text('Route: Foo')));
int notifyCount = 0;
route1.restorationScopeId.addListener(() {
notifyCount++;
});
expect(route1.isActive, isTrue);
expect(route1.restorationScopeId.value, isNotNull);
expect(route1.animation.isCompleted, isTrue);
expect(notifyCount, 0);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
expect(notifyCount, 1);
await tester.pump();
await tester.pump();
// Pop is in progress.
expect(route1.restorationScopeId.value, isNull);
expect(route1.settings.name, 'Foo');
expect(route1.animation.isCompleted, isFalse);
expect(route1.animation.isDismissed, isFalse);
expect(route1.isActive, isFalse);
await tester.restartAndRestore();
expect(findRoute('Foo', skipOffstage: false), findsNothing);
expect(findRoute('home', count: 1), findsOneWidget);
expect(notifyCount, 1);
});
testWidgets('routes are restored in the right order', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route1');
await tester.pumpAndSettle();
expect(findRoute('route1'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route2');
await tester.pumpAndSettle();
expect(findRoute('route2'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route3');
await tester.pumpAndSettle();
expect(findRoute('route3'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route4');
await tester.pumpAndSettle();
expect(findRoute('route4'), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('route4'), findsOneWidget);
expect(findRoute('route3', skipOffstage: false), findsOneWidget);
expect(findRoute('route2', skipOffstage: false), findsOneWidget);
expect(findRoute('route1', skipOffstage: false), findsOneWidget);
expect(findRoute('home', skipOffstage: false), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('route3'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('route2'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('route1'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('home'), findsOneWidget);
});
testWidgets('all routes up to first unrestorable are restored', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route1');
await tester.pumpAndSettle();
expect(findRoute('route1'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route2');
await tester.pumpAndSettle();
expect(findRoute('route2'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('route3');
await tester.pumpAndSettle();
expect(findRoute('route3'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route4');
await tester.pumpAndSettle();
expect(findRoute('route4'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeBuilder, arguments: 'route5');
await tester.pumpAndSettle();
expect(findRoute('route5'), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('route5', skipOffstage: false), findsNothing);
expect(findRoute('route4', skipOffstage: false), findsNothing);
expect(findRoute('route3', skipOffstage: false), findsNothing);
expect(findRoute('route2'), findsOneWidget);
expect(findRoute('route1', skipOffstage: false), findsOneWidget);
expect(findRoute('home', skipOffstage: false), findsOneWidget);
});
testWidgets('removing unrestorable routes restores all of them', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route1');
await tester.pumpAndSettle();
expect(findRoute('route1'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route2');
await tester.pumpAndSettle();
expect(findRoute('route2'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('route3');
await tester.pumpAndSettle();
expect(findRoute('route3'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route4');
await tester.pumpAndSettle();
expect(findRoute('route4'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('route5');
await tester.pumpAndSettle();
expect(findRoute('route5'), findsOneWidget);
final Route<Object> route = ModalRoute.of(tester.element(find.text('Route: route3', skipOffstage: false)));
expect(route.settings.name, 'route3');
tester.state<NavigatorState>(find.byType(Navigator)).removeRoute(route);
await tester.pumpAndSettle();
await tester.restartAndRestore();
expect(findRoute('route5'), findsOneWidget);
expect(findRoute('route4', skipOffstage: false), findsOneWidget);
expect(findRoute('route3', skipOffstage: false), findsNothing);
expect(findRoute('route2', skipOffstage: false), findsOneWidget);
expect(findRoute('route1', skipOffstage: false), findsOneWidget);
expect(findRoute('home', skipOffstage: false), findsOneWidget);
});
testWidgets('RestorableRouteFuture', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeFutureBuilder);
await tester.pumpAndSettle();
expect(find.text('Return value: null'), findsOneWidget);
final RestorableRouteFuture<int> routeFuture = tester
.state<RouteFutureWidgetState>(find.byType(RouteFutureWidget))
.routeFuture;
expect(routeFuture.route, isNull);
expect(routeFuture.isPresent, isFalse);
expect(routeFuture.enabled, isFalse);
routeFuture.present('Foo');
await tester.pumpAndSettle();
expect(find.text('Route: Foo'), findsOneWidget);
expect(routeFuture.route.settings.name, 'Foo');
expect(routeFuture.isPresent, isTrue);
expect(routeFuture.enabled, isTrue);
await tester.restartAndRestore();
expect(find.text('Route: Foo'), findsOneWidget);
final RestorableRouteFuture<int> restoredRouteFuture = tester
.state<RouteFutureWidgetState>(find.byType(RouteFutureWidget, skipOffstage: false))
.routeFuture;
expect(restoredRouteFuture.route.settings.name, 'Foo');
expect(restoredRouteFuture.isPresent, isTrue);
expect(restoredRouteFuture.enabled, isTrue);
tester.state<NavigatorState>(find.byType(Navigator)).pop(10);
await tester.pumpAndSettle();
expect(find.text('Return value: 10'), findsOneWidget);
expect(restoredRouteFuture.route, isNull);
expect(restoredRouteFuture.isPresent, isFalse);
expect(restoredRouteFuture.enabled, isFalse);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
testWidgets('RestorableRouteFuture in unrestorable context', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
expect(findRoute('home'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pushNamed('unrestorable');
await tester.pumpAndSettle();
expect(findRoute('unrestorable'), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeFutureBuilder);
await tester.pumpAndSettle();
expect(find.text('Return value: null'), findsOneWidget);
final RestorableRouteFuture<int> routeFuture = tester
.state<RouteFutureWidgetState>(find.byType(RouteFutureWidget))
.routeFuture;
expect(routeFuture.route, isNull);
expect(routeFuture.isPresent, isFalse);
expect(routeFuture.enabled, isFalse);
routeFuture.present('Foo');
await tester.pumpAndSettle();
expect(find.text('Route: Foo'), findsOneWidget);
expect(routeFuture.route.settings.name, 'Foo');
expect(routeFuture.isPresent, isTrue);
expect(routeFuture.enabled, isFalse);
await tester.restartAndRestore();
expect(findRoute('home'), findsOneWidget);
});
testWidgets('Illegal arguments throw', (WidgetTester tester) async {
await tester.pumpWidget(const TestWidget());
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Bar');
await tester.pumpAndSettle();
final Route<Object> oldRoute = ModalRoute.of(tester.element(find.text('Route: Bar')));
expect(oldRoute.settings.name, 'Bar');
final Matcher throwsArgumentsAssertionError = throwsA(isAssertionError.having(
(AssertionError e) => e.message,
'message',
'The arguments object must be serializable via the StandardMessageCodec.',
));
final Matcher throwsBuilderAssertionError = throwsA(isAssertionError.having(
(AssertionError e) => e.message,
'message',
'The provided routeBuilder must be a static function.',
));
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo', arguments: Object()),
throwsArgumentsAssertionError,
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacementNamed('Foo', arguments: Object()),
throwsArgumentsAssertionError,
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorablePopAndPushNamed('Foo', arguments: Object()),
throwsArgumentsAssertionError,
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamedAndRemoveUntil('Foo', (Route<Object> _) => false, arguments: Object()),
throwsArgumentsAssertionError,
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorablePush(_routeBuilder, arguments: Object()),
throwsArgumentsAssertionError,
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacement(_routeBuilder, arguments: Object()),
throwsArgumentsAssertionError,
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushAndRemoveUntil(_routeBuilder, (Route<Object> _) => false, arguments: Object()),
throwsArgumentsAssertionError,
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorableReplace(newRouteBuilder: _routeBuilder, oldRoute: oldRoute, arguments: Object()),
throwsArgumentsAssertionError,
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorableReplaceRouteBelow(newRouteBuilder: _routeBuilder, anchorRoute: oldRoute, arguments: Object()),
throwsArgumentsAssertionError,
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorablePush((BuildContext _, Object __) => null),
throwsBuilderAssertionError,
skip: isBrowser, // https://github.com/flutter/flutter/issues/33615
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushReplacement((BuildContext _, Object __) => null),
throwsBuilderAssertionError,
skip: isBrowser, // https://github.com/flutter/flutter/issues/33615
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorablePushAndRemoveUntil((BuildContext _, Object __) => null, (Route<Object> _) => false),
throwsBuilderAssertionError,
skip: isBrowser, // https://github.com/flutter/flutter/issues/33615
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorableReplace(newRouteBuilder: (BuildContext _, Object __) => null, oldRoute: oldRoute),
throwsBuilderAssertionError,
skip: isBrowser, // https://github.com/flutter/flutter/issues/33615
);
expect(
() => tester.state<NavigatorState>(find.byType(Navigator)).restorableReplaceRouteBelow(newRouteBuilder: (BuildContext _, Object __) => null, anchorRoute: oldRoute),
throwsBuilderAssertionError,
skip: isBrowser, // https://github.com/flutter/flutter/issues/33615
);
});
testWidgets('Moving scopes', (WidgetTester tester) async {
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root',
child: TestWidget(
restorationId: null,
),
));
await tapRouteCounter('home', tester);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
await tester.pumpAndSettle();
expect(findRoute('Foo'), findsOneWidget);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
// Nothing is restored.
await tester.restartAndRestore();
expect(findRoute('Foo'), findsNothing);
expect(findRoute('home', count: 0), findsOneWidget);
await tapRouteCounter('home', tester);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
await tester.pumpAndSettle();
// Move navigator into restoration scope.
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root',
child: TestWidget(
restorationId: 'app',
),
));
expect(findRoute('Foo'), findsOneWidget);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
// Everything is restored.
await tester.restartAndRestore();
expect(findRoute('Foo'), findsOneWidget);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
// Move navigator out of restoration scope.
await tester.pumpWidget(const RootRestorationScope(
restorationId: 'root',
child: TestWidget(
restorationId: null,
),
));
expect(findRoute('Foo'), findsOneWidget);
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
// Nothing is restored.
await tester.restartAndRestore();
expect(findRoute('Foo'), findsNothing);
expect(findRoute('home', count: 0), findsOneWidget);
});
testWidgets('Restoring pages', (WidgetTester tester) async {
await tester.pumpWidget(const PagedTestWidget());
expect(findRoute('home', count: 0), findsOneWidget);
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
await tester.pumpAndSettle();
await tapRouteCounter('Foo', tester);
await tapRouteCounter('Foo', tester);
expect(findRoute('Foo', count: 2), findsOneWidget);
final TestRestorationData data = await tester.getRestorationData();
await tester.restartAndRestore();
expect(findRoute('Foo', count: 2), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('bar');
await tester.pumpAndSettle();
await tapRouteCounter('bar', tester);
expect(findRoute('bar', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('Foo');
await tester.pumpAndSettle();
expect(findRoute('Foo', count: 0), findsOneWidget);
await tester.restoreFrom(data);
expect(findRoute('bar', skipOffstage: false), findsNothing);
expect(findRoute('Foo', count: 2), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('bar');
await tester.pumpAndSettle();
expect(findRoute('bar', count: 0), findsOneWidget);
});
testWidgets('Unrestorable pages', (WidgetTester tester) async {
await tester.pumpWidget(const PagedTestWidget());
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p1');
await tester.pumpAndSettle();
await tapRouteCounter('p1', tester);
expect(findRoute('p1', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('r1');
await tester.pumpAndSettle();
await tapRouteCounter('r1', tester);
expect(findRoute('r1', count: 1), findsOneWidget);
tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p2', restoreState: false);
await tester.pumpAndSettle();
await tapRouteCounter('p2', tester);
expect(findRoute('p2', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('r2');
await tester.pumpAndSettle();
await tapRouteCounter('r2', tester);
expect(findRoute('r2', count: 1), findsOneWidget);
tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p3');
await tester.pumpAndSettle();
await tapRouteCounter('p3', tester);
expect(findRoute('p3', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('r3');
await tester.pumpAndSettle();
await tapRouteCounter('r3', tester);
expect(findRoute('r3', count: 1), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('r2', skipOffstage: false), findsNothing);
expect(findRoute('r3', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('p3', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('p2', count: 0), findsOneWidget); // Page did not restore its state!
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('r1', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('p1', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).pop();
await tester.pumpAndSettle();
expect(findRoute('home', count: 1), findsOneWidget);
});
testWidgets('removed page is not restored', (WidgetTester tester) async {
await tester.pumpWidget(const PagedTestWidget());
await tapRouteCounter('home', tester);
expect(findRoute('home', count: 1), findsOneWidget);
tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p1');
await tester.pumpAndSettle();
await tapRouteCounter('p1', tester);
expect(findRoute('p1', count: 1), findsOneWidget);
tester.state<NavigatorState>(find.byType(Navigator)).restorablePushNamed('r1');
await tester.pumpAndSettle();
await tapRouteCounter('r1', tester);
expect(findRoute('r1', count: 1), findsOneWidget);
tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p2');
await tester.pumpAndSettle();
await tapRouteCounter('p2', tester);
expect(findRoute('p2', count: 1), findsOneWidget);
tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).removePage('p1');
await tester.pumpAndSettle();
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
expect(findRoute('p1', count: 1, skipOffstage: false), findsNothing);
expect(findRoute('r1', count: 1, skipOffstage: false), findsNothing);
expect(findRoute('p2', count: 1), findsOneWidget);
await tester.restartAndRestore();
expect(findRoute('home', count: 1, skipOffstage: false), findsOneWidget);
expect(findRoute('p1', count: 1, skipOffstage: false), findsNothing);
expect(findRoute('r1', count: 1, skipOffstage: false), findsNothing);
expect(findRoute('p2', count: 1), findsOneWidget);
tester.state<PagedTestNavigatorState>(find.byType(PagedTestNavigator)).addPage('p1');
await tester.pumpAndSettle();
expect(findRoute('p1', count: 0), findsOneWidget);
});
}
Route<void> _routeBuilder(BuildContext context, Object arguments) {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return RouteWidget(
name: arguments as String,
);
},
);
}
Route<void> _routeFutureBuilder(BuildContext context, Object arguments) {
return MaterialPageRoute<void>(
builder: (BuildContext context) {
return RouteFutureWidget();
},
);
}
class PagedTestWidget extends StatelessWidget {
const PagedTestWidget({this.restorationId = 'app'});
final String restorationId;
@override
Widget build(BuildContext context) {
return RootRestorationScope(
restorationId: restorationId,
child: Directionality(
textDirection: TextDirection.ltr,
child: PagedTestNavigator(),
),
);
}
}
class PagedTestNavigator extends StatefulWidget {
@override
State<PagedTestNavigator> createState() => PagedTestNavigatorState();
}
class PagedTestNavigatorState extends State<PagedTestNavigator> with RestorationMixin {
final RestorableString _routes = RestorableString('r-home');
void addPage(String name, {bool restoreState = true, int index}) {
assert(!name.contains(','));
assert(!name.startsWith('r-'));
final List<String> routes = _routes.value.split(',');
name = restoreState ? 'r-$name' : name;
if (index != null) {
routes.insert(index, name);
} else {
routes.add(name);
}
setState(() {
_routes.value = routes.join(',');
});
}
bool removePage(String name) {
final List<String> routes = _routes.value.split(',');
if (routes.remove(name) || routes.remove('r-$name')) {
setState(() {
_routes.value = routes.join(',');
});
return true;
}
return false;
}
@override
Widget build(BuildContext context) {
return Navigator(
restorationScopeId: 'nav',
onPopPage: (Route<dynamic> route, dynamic result) {
if (route.didPop(result)) {
removePage(route.settings.name);
return true;
}
return false;
},
pages: _routes.value.isEmpty ? const <Page<Object>>[] : _routes.value.split(',').map((String name) {
if (name.startsWith('r-')) {
name = name.substring(2);
return TestPage(
name: name,
restorationId: name,
key: ValueKey<String>(name),
);
}
return TestPage(
name: name,
key: ValueKey<String>(name),
);
}).toList(),
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<int>(
settings: settings,
builder: (BuildContext context) {
return RouteWidget(
name: settings.name,
arguments: settings.arguments,
);
},
);
},
);
}
@override
String get restorationId => 'router';
@override
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(_routes, 'routes');
}
@override
void dispose() {
super.dispose();
_routes.dispose();
}
}
class TestPage extends Page<void> {
const TestPage({LocalKey key, String name, String restorationId}) : super(name: name, key: key, restorationId: restorationId);
@override
Route<void> createRoute(BuildContext context) {
return MaterialPageRoute<void>(
settings: this,
builder: (BuildContext context) {
return RouteWidget(
name: name,
);
}
);
}
}
class TestWidget extends StatelessWidget {
const TestWidget({this.restorationId = 'app'});
final String restorationId;
@override
Widget build(BuildContext context) {
return RootRestorationScope(
restorationId: restorationId,
child: Directionality(
textDirection: TextDirection.ltr,
child: Navigator(
initialRoute: 'home',
restorationScopeId: 'app',
onGenerateRoute: (RouteSettings settings) {
return MaterialPageRoute<int>(
settings: settings,
builder: (BuildContext context) {
return RouteWidget(
name: settings.name,
arguments: settings.arguments,
);
},
);
},
),
),
);
}
}
class RouteWidget extends StatefulWidget {
const RouteWidget({Key key, this.name, this.arguments}) : super(key: key);
final String name;
final Object arguments;
@override
State<RouteWidget> createState() => RouteWidgetState();
}
class RouteWidgetState extends State<RouteWidget> with RestorationMixin {
final RestorableInt counter = RestorableInt(0);
@override
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(counter, 'counter');
}
@override
String get restorationId => 'stateful';
@override
void dispose() {
super.dispose();
counter.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Column(
children: <Widget>[
GestureDetector(
child: Text('Route: ${widget.name}'),
onTap: () {
setState(() {
counter.value++;
});
},
),
if (widget.arguments != null)
Text('Arguments(home): ${widget.arguments}'),
Text('Counter(${widget.name}): ${counter.value}'),
],
),
);
}
}
class RouteFutureWidget extends StatefulWidget {
@override
State<RouteFutureWidget> createState() => RouteFutureWidgetState();
}
class RouteFutureWidgetState extends State<RouteFutureWidget> with RestorationMixin {
RestorableRouteFuture<int> routeFuture;
int value;
@override
void initState() {
super.initState();
routeFuture = RestorableRouteFuture<int>(
onPresent: (NavigatorState navigatorState, Object arguments) {
return navigatorState.restorablePushNamed(arguments as String);
},
onComplete: (int i) {
setState(() {
value = i;
});
}
);
}
@override
void restoreState(RestorationBucket oldBucket, bool initialRestore) {
registerForRestoration(routeFuture, 'routeFuture');
}
@override
String get restorationId => 'routefuturewidget';
@override
void dispose() {
super.dispose();
routeFuture.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: Text('Return value: $value'),
);
}
}
Finder findRoute(String name, { Object arguments, int count, bool skipOffstage = true }) => _RouteFinder(name, arguments: arguments, count: count, skipOffstage: skipOffstage);
Future<void> tapRouteCounter(String name, WidgetTester tester) async {
await tester.tap(find.text('Route: $name'));
await tester.pump();
}
class _RouteFinder extends MatchFinder {
_RouteFinder(this.name, { this.arguments, this.count, bool skipOffstage = true }) : super(skipOffstage: skipOffstage);
final String name;
final Object arguments;
final int count;
@override
String get description {
String result = 'Route(name: $name';
if (arguments != null) {
result += ', arguments: $arguments';
}
if (count != null) {
result += ', count: $count';
}
return result;
}
@override
bool matches(Element candidate) {
final Widget widget = candidate.widget;
if (widget is RouteWidget) {
if (widget.name != name) {
return false;
}
if (arguments != null && widget.arguments != arguments) {
return false;
}
final RouteWidgetState state = (candidate as StatefulElement).state as RouteWidgetState;
if (count != null && state.counter.value != count) {
return false;
}
return true;
}
return false;
}
}
......@@ -1664,7 +1664,7 @@ void main() {
' The onGenerateRoute callback must never return null, unless an\n'
' onUnknownRoute callback is provided as well.\n'
' The Navigator was:\n'
' NavigatorState#4d6bf(lifecycle state: created)\n',
' NavigatorState#00000(lifecycle state: initialized)\n'
),
);
});
......@@ -1690,7 +1690,7 @@ void main() {
' route "/".\n'
' The onUnknownRoute callback must never return null.\n'
' The Navigator was:\n'
' NavigatorState#38036(lifecycle state: created)\n',
' NavigatorState#00000(lifecycle state: initialized)\n',
),
);
});
......
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