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,
),
),
),
),
......
......@@ -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,
),
),
),
......
This diff is collapsed.
......@@ -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