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 { ...@@ -94,6 +94,7 @@ class CupertinoApp extends StatefulWidget {
this.debugShowCheckedModeBanner = true, this.debugShowCheckedModeBanner = true,
this.shortcuts, this.shortcuts,
this.actions, this.actions,
this.restorationScopeId,
}) : assert(routes != null), }) : assert(routes != null),
assert(navigatorObservers != null), assert(navigatorObservers != null),
assert(title != null), assert(title != null),
...@@ -132,6 +133,7 @@ class CupertinoApp extends StatefulWidget { ...@@ -132,6 +133,7 @@ class CupertinoApp extends StatefulWidget {
this.debugShowCheckedModeBanner = true, this.debugShowCheckedModeBanner = true,
this.shortcuts, this.shortcuts,
this.actions, this.actions,
this.restorationScopeId,
}) : assert(title != null), }) : assert(title != null),
assert(showPerformanceOverlay != null), assert(showPerformanceOverlay != null),
assert(checkerboardRasterCacheImages != null), assert(checkerboardRasterCacheImages != null),
...@@ -315,6 +317,9 @@ class CupertinoApp extends StatefulWidget { ...@@ -315,6 +317,9 @@ class CupertinoApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso} /// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
final Map<Type, Action<Intent>>? actions; final Map<Type, Action<Intent>>? actions;
/// {@macro flutter.widgets.widgetsApp.restorationScopeId}
final String? restorationScopeId;
@override @override
_CupertinoAppState createState() => _CupertinoAppState(); _CupertinoAppState createState() => _CupertinoAppState();
...@@ -400,6 +405,7 @@ class _CupertinoAppState extends State<CupertinoApp> { ...@@ -400,6 +405,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder, inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts, shortcuts: widget.shortcuts,
actions: widget.actions, actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
); );
} }
return WidgetsApp( return WidgetsApp(
...@@ -433,6 +439,7 @@ class _CupertinoAppState extends State<CupertinoApp> { ...@@ -433,6 +439,7 @@ class _CupertinoAppState extends State<CupertinoApp> {
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder, inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts, shortcuts: widget.shortcuts,
actions: widget.actions, actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
); );
} }
......
...@@ -197,6 +197,7 @@ class MaterialApp extends StatefulWidget { ...@@ -197,6 +197,7 @@ class MaterialApp extends StatefulWidget {
this.debugShowCheckedModeBanner = true, this.debugShowCheckedModeBanner = true,
this.shortcuts, this.shortcuts,
this.actions, this.actions,
this.restorationScopeId,
}) : assert(routes != null), }) : assert(routes != null),
assert(navigatorObservers != null), assert(navigatorObservers != null),
assert(title != null), assert(title != null),
...@@ -241,6 +242,7 @@ class MaterialApp extends StatefulWidget { ...@@ -241,6 +242,7 @@ class MaterialApp extends StatefulWidget {
this.debugShowCheckedModeBanner = true, this.debugShowCheckedModeBanner = true,
this.shortcuts, this.shortcuts,
this.actions, this.actions,
this.restorationScopeId,
}) : assert(routeInformationParser != null), }) : assert(routeInformationParser != null),
assert(routerDelegate != null), assert(routerDelegate != null),
assert(title != null), assert(title != null),
...@@ -620,6 +622,9 @@ class MaterialApp extends StatefulWidget { ...@@ -620,6 +622,9 @@ class MaterialApp extends StatefulWidget {
/// {@macro flutter.widgets.widgetsApp.actions.seeAlso} /// {@macro flutter.widgets.widgetsApp.actions.seeAlso}
final Map<Type, Action<Intent>> actions; final Map<Type, Action<Intent>> actions;
/// {@macro flutter.widgets.widgetsApp.restorationScopeId}
final String restorationScopeId;
/// Turns on a [GridPaper] overlay that paints a baseline grid /// Turns on a [GridPaper] overlay that paints a baseline grid
/// Material apps. /// Material apps.
/// ///
...@@ -780,6 +785,7 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -780,6 +785,7 @@ class _MaterialAppState extends State<MaterialApp> {
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder, inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts, shortcuts: widget.shortcuts,
actions: widget.actions, actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
); );
} }
...@@ -814,6 +820,7 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -814,6 +820,7 @@ class _MaterialAppState extends State<MaterialApp> {
inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder, inspectorSelectButtonBuilder: _inspectorSelectButtonBuilder,
shortcuts: widget.shortcuts, shortcuts: widget.shortcuts,
actions: widget.actions, actions: widget.actions,
restorationScopeId: widget.restorationScopeId,
); );
} }
......
...@@ -19,6 +19,7 @@ import 'media_query.dart'; ...@@ -19,6 +19,7 @@ import 'media_query.dart';
import 'navigator.dart'; import 'navigator.dart';
import 'pages.dart'; import 'pages.dart';
import 'performance_overlay.dart'; import 'performance_overlay.dart';
import 'restoration.dart';
import 'router.dart'; import 'router.dart';
import 'scrollable.dart'; import 'scrollable.dart';
import 'semantics_debugger.dart'; import 'semantics_debugger.dart';
...@@ -195,6 +196,7 @@ class WidgetsApp extends StatefulWidget { ...@@ -195,6 +196,7 @@ class WidgetsApp extends StatefulWidget {
this.inspectorSelectButtonBuilder, this.inspectorSelectButtonBuilder,
this.shortcuts, this.shortcuts,
this.actions, this.actions,
this.restorationScopeId,
}) : assert(navigatorObservers != null), }) : assert(navigatorObservers != null),
assert(routes != null), assert(routes != null),
assert( assert(
...@@ -290,6 +292,7 @@ class WidgetsApp extends StatefulWidget { ...@@ -290,6 +292,7 @@ class WidgetsApp extends StatefulWidget {
this.inspectorSelectButtonBuilder, this.inspectorSelectButtonBuilder,
this.shortcuts, this.shortcuts,
this.actions, this.actions,
this.restorationScopeId,
}) : assert( }) : assert(
routeInformationParser != null && routeInformationParser != null &&
routerDelegate != null, routerDelegate != null,
...@@ -945,6 +948,24 @@ class WidgetsApp extends StatefulWidget { ...@@ -945,6 +948,24 @@ class WidgetsApp extends StatefulWidget {
/// {@endtemplate} /// {@endtemplate}
final Map<Type, Action<Intent>>? actions; 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. /// If true, forces the performance overlay to be visible in all instances.
/// ///
/// Used by the `showPerformanceOverlay` observatory extension. /// Used by the `showPerformanceOverlay` observatory extension.
...@@ -1467,6 +1488,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1467,6 +1488,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
} else { } else {
assert(_navigator != null); assert(_navigator != null);
routing = Navigator( routing = Navigator(
restorationScopeId: 'nav',
key: _navigator, key: _navigator,
initialRoute: _initialRouteName, initialRoute: _initialRouteName,
onGenerateRoute: _onGenerateRoute, onGenerateRoute: _onGenerateRoute,
...@@ -1573,7 +1595,9 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1573,7 +1595,9 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
: _locale!; : _locale!;
assert(_debugCheckLocalizations(appLocale)); assert(_debugCheckLocalizations(appLocale));
return Shortcuts( return RootRestorationScope(
restorationId: widget.restorationScopeId,
child: Shortcuts(
shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts, shortcuts: widget.shortcuts ?? WidgetsApp.defaultShortcuts,
debugLabel: '<Default WidgetsApp Shortcuts>', debugLabel: '<Default WidgetsApp Shortcuts>',
child: Actions( child: Actions(
...@@ -1589,6 +1613,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver { ...@@ -1589,6 +1613,7 @@ class _WidgetsAppState extends State<WidgetsApp> with WidgetsBindingObserver {
), ),
), ),
), ),
),
); );
} }
} }
......
...@@ -499,10 +499,13 @@ abstract class RestorableProperty<T> extends ChangeNotifier { ...@@ -499,10 +499,13 @@ abstract class RestorableProperty<T> extends ChangeNotifier {
} }
/// The [State] object that this property is registered with. /// The [State] object that this property is registered with.
///
/// Must only be called when [isRegistered] is true.
@protected @protected
State? get state { State get state {
assert(isRegistered);
assert(_debugAssertNotDisposed()); assert(_debugAssertNotDisposed());
return _owner; return _owner!;
} }
/// Whether this property is currently registered with a [RestorationMixin]. /// Whether this property is currently registered with a [RestorationMixin].
...@@ -609,13 +612,10 @@ abstract class RestorableProperty<T> extends ChangeNotifier { ...@@ -609,13 +612,10 @@ abstract class RestorableProperty<T> extends ChangeNotifier {
/// class RestorationExampleApp extends StatelessWidget { /// class RestorationExampleApp extends StatelessWidget {
/// @override /// @override
/// Widget build(BuildContext context) { /// Widget build(BuildContext context) {
/// // The [RootRestorationScope] can be removed once it is part of [MaterialApp]. /// return MaterialApp(
/// return RootRestorationScope( /// restorationScopeId: 'app',
/// restorationId: 'root',
/// child: MaterialApp(
/// title: 'Restorable Counter', /// title: 'Restorable Counter',
/// home: RestorableCounter(restorationId: 'counter'), /// home: RestorableCounter(restorationId: 'counter'),
/// ),
/// ); /// );
/// } /// }
/// } /// }
......
...@@ -18,6 +18,7 @@ import 'modal_barrier.dart'; ...@@ -18,6 +18,7 @@ import 'modal_barrier.dart';
import 'navigator.dart'; import 'navigator.dart';
import 'overlay.dart'; import 'overlay.dart';
import 'page_storage.dart'; import 'page_storage.dart';
import 'restoration.dart';
import 'transitions.dart'; import 'transitions.dart';
// Examples can assume: // Examples can assume:
...@@ -773,7 +774,16 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -773,7 +774,16 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _ModalScopeStatus( 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, route: widget.route,
isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates isCurrent: widget.route.isCurrent, // _routeSetState is called if this updates
canPop: widget.route.canPop, // _routeSetState is called if this updates canPop: widget.route.canPop, // _routeSetState is called if this updates
...@@ -829,6 +839,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> { ...@@ -829,6 +839,7 @@ class _ModalScopeState<T> extends State<_ModalScope<T>> {
), ),
), ),
), ),
),
); );
} }
} }
......
...@@ -97,6 +97,10 @@ void main() { ...@@ -97,6 +97,10 @@ void main() {
expect(tester.takeException(), isFlutterError); expect(tester.takeException(), isFlutterError);
expect(unknownForRouteCalled, '/'); 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 { testWidgets('Can use navigatorKey to navigate', (WidgetTester tester) async {
......
...@@ -16,9 +16,9 @@ const String alternativeText = 'Everything is awesome!!'; ...@@ -16,9 +16,9 @@ const String alternativeText = 'Everything is awesome!!';
void main() { void main() {
testWidgets('CupertinoTextField restoration', (WidgetTester tester) async { testWidgets('CupertinoTextField restoration', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const RootRestorationScope( const CupertinoApp(
child: TestWidget(), restorationScopeId: 'app',
restorationId: 'root', home: TestWidget(),
), ),
); );
...@@ -27,11 +27,11 @@ void main() { ...@@ -27,11 +27,11 @@ void main() {
testWidgets('CupertinoTextField restoration with external controller', (WidgetTester tester) async { testWidgets('CupertinoTextField restoration with external controller', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const RootRestorationScope( const CupertinoApp(
child: TestWidget( restorationScopeId: 'app',
home: TestWidget(
useExternal: true, useExternal: true,
), ),
restorationId: 'root',
), ),
); );
...@@ -102,8 +102,7 @@ class TestWidgetState extends State<TestWidget> with RestorationMixin { ...@@ -102,8 +102,7 @@ class TestWidgetState extends State<TestWidget> with RestorationMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return Material(
home: Material(
child: Align( child: Align(
alignment: Alignment.center, alignment: Alignment.center,
child: SizedBox( child: SizedBox(
...@@ -115,7 +114,6 @@ class TestWidgetState extends State<TestWidget> with RestorationMixin { ...@@ -115,7 +114,6 @@ class TestWidgetState extends State<TestWidget> with RestorationMixin {
), ),
), ),
), ),
),
); );
} }
} }
......
...@@ -384,6 +384,10 @@ void main() { ...@@ -384,6 +384,10 @@ void main() {
); );
expect(tester.takeException(), isFlutterError); expect(tester.takeException(), isFlutterError);
expect(log, <String>['onGenerateRoute /', 'onUnknownRoute /']); 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 { testWidgets('MaterialApp with builder and no route information works.', (WidgetTester tester) async {
......
...@@ -142,6 +142,9 @@ void main() { ...@@ -142,6 +142,9 @@ void main() {
' PageStorage\n' ' PageStorage\n'
' Offstage\n' ' Offstage\n'
' _ModalScopeStatus\n' ' _ModalScopeStatus\n'
' UnmanagedRestorationScope\n'
' RestorationScope\n'
' AnimatedBuilder\n'
' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#00000]\n' ' _ModalScope<dynamic>-[LabeledGlobalKey<_ModalScopeState<dynamic>>#00000]\n'
' Semantics\n' ' Semantics\n'
' _EffectiveTickerMode\n' ' _EffectiveTickerMode\n'
...@@ -149,6 +152,7 @@ void main() { ...@@ -149,6 +152,7 @@ void main() {
' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#00000]\n' ' _OverlayEntryWidget-[LabeledGlobalKey<_OverlayEntryWidgetState>#00000]\n'
' _Theatre\n' ' _Theatre\n'
' Overlay-[LabeledGlobalKey<OverlayState>#00000]\n' ' Overlay-[LabeledGlobalKey<OverlayState>#00000]\n'
' UnmanagedRestorationScope\n'
' _FocusMarker\n' ' _FocusMarker\n'
' Semantics\n' ' Semantics\n'
' FocusScope\n' ' FocusScope\n'
...@@ -187,6 +191,10 @@ void main() { ...@@ -187,6 +191,10 @@ void main() {
' _FocusMarker\n' ' _FocusMarker\n'
' Focus\n' ' Focus\n'
' Shortcuts\n' ' Shortcuts\n'
' UnmanagedRestorationScope\n'
' RestorationScope\n'
' UnmanagedRestorationScope\n'
' RootRestorationScope\n'
' WidgetsApp-[GlobalObjectKey _MaterialAppState#00000]\n' ' WidgetsApp-[GlobalObjectKey _MaterialAppState#00000]\n'
' HeroControllerScope\n' ' HeroControllerScope\n'
' ScrollConfiguration\n' ' ScrollConfiguration\n'
......
...@@ -15,9 +15,9 @@ const String alternativeText = 'Everything is awesome!!'; ...@@ -15,9 +15,9 @@ const String alternativeText = 'Everything is awesome!!';
void main() { void main() {
testWidgets('TextField restoration', (WidgetTester tester) async { testWidgets('TextField restoration', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const RootRestorationScope( const MaterialApp(
child: TestWidget(), restorationScopeId: 'app',
restorationId: 'root', home: TestWidget(),
), ),
); );
...@@ -26,11 +26,11 @@ void main() { ...@@ -26,11 +26,11 @@ void main() {
testWidgets('TextField restoration with external controller', (WidgetTester tester) async { testWidgets('TextField restoration with external controller', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
const RootRestorationScope( const MaterialApp(
child: TestWidget( restorationScopeId: 'root',
home: TestWidget(
useExternal: true, useExternal: true,
), ),
restorationId: 'root',
), ),
); );
...@@ -101,8 +101,7 @@ class TestWidgetState extends State<TestWidget> with RestorationMixin { ...@@ -101,8 +101,7 @@ class TestWidgetState extends State<TestWidget> with RestorationMixin {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return Material(
home: Material(
child: Align( child: Align(
alignment: Alignment.center, alignment: Alignment.center,
child: SizedBox( child: SizedBox(
...@@ -114,7 +113,6 @@ class TestWidgetState extends State<TestWidget> with RestorationMixin { ...@@ -114,7 +113,6 @@ class TestWidgetState extends State<TestWidget> with RestorationMixin {
), ),
), ),
), ),
),
); );
} }
} }
......
This diff is collapsed.
...@@ -1664,7 +1664,7 @@ void main() { ...@@ -1664,7 +1664,7 @@ void main() {
' The onGenerateRoute callback must never return null, unless an\n' ' The onGenerateRoute callback must never return null, unless an\n'
' onUnknownRoute callback is provided as well.\n' ' onUnknownRoute callback is provided as well.\n'
' The Navigator was:\n' ' The Navigator was:\n'
' NavigatorState#4d6bf(lifecycle state: created)\n', ' NavigatorState#00000(lifecycle state: initialized)\n'
), ),
); );
}); });
...@@ -1690,7 +1690,7 @@ void main() { ...@@ -1690,7 +1690,7 @@ void main() {
' route "/".\n' ' route "/".\n'
' The onUnknownRoute callback must never return null.\n' ' The onUnknownRoute callback must never return null.\n'
' The Navigator was:\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