Unverified Commit 5801f0e5 authored by Shi-Hao Hong's avatar Shi-Hao Hong Committed by GitHub

Expose DialogRoutes for state restoration support (#73829)

parent ea3aa674
......@@ -1125,6 +1125,78 @@ Widget _buildCupertinoDialogTransitions(BuildContext context, Animation<double>
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
/// ### State Restoration in Dialogs
///
/// Using this method will not enable state restoration for the dialog. In order
/// to enable state restoration for a dialog, use [Navigator.restorablePush]
/// or [Navigator.restorablePushNamed] with [CupertinoDialogRoute].
///
/// For more information about state restoration, see [RestorationManager].
///
/// {@tool sample --template=freeform}
///
/// This sample demonstrates how to create a restorable Cupertino dialog. This is
/// accomplished by enabling state restoration by specifying
/// [CupertinoApp.restorationScopeId] and using [Navigator.restorablePush] to
/// push [CupertinoDialogRoute] when the [CupertinoButton] is tapped.
///
/// {@macro flutter.widgets.RestorationManager}
///
/// ```dart imports
/// import 'package:flutter/cupertino.dart';
/// ```
///
/// ```dart
/// void main() {
/// runApp(MyApp());
/// }
///
/// class MyApp extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return CupertinoApp(
/// restorationScopeId: 'app',
/// home: MyHomePage(),
/// );
/// }
/// }
///
/// class MyHomePage extends StatelessWidget {
/// static Route<Object?> _dialogBuilder(BuildContext context, Object? arguments) {
/// return CupertinoDialogRoute<void>(
/// context: context,
/// builder: (BuildContext context) {
/// return const CupertinoAlertDialog(
/// title: Text('Title'),
/// content: Text('Content'),
/// actions: <Widget>[
/// CupertinoDialogAction(child: Text('Yes')),
/// CupertinoDialogAction(child: Text('No')),
/// ],
/// );
/// },
/// );
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return CupertinoPageScaffold(
/// navigationBar: const CupertinoNavigationBar(
/// middle: Text('Home'),
/// ),
/// child: Center(child: CupertinoButton(
/// onPressed: () {
/// Navigator.of(context).restorablePush(_dialogBuilder);
/// },
/// child: const Text('Open Dialog'),
/// )),
/// );
/// }
/// }
/// ```
///
/// {@end-tool}
///
/// See also:
///
/// * [CupertinoAlertDialog], an iOS-style alert dialog.
......@@ -1134,24 +1206,79 @@ Widget _buildCupertinoDialogTransitions(BuildContext context, Animation<double>
Future<T?> showCupertinoDialog<T>({
required BuildContext context,
required WidgetBuilder builder,
String? barrierLabel,
bool useRootNavigator = true,
bool barrierDismissible = false,
RouteSettings? routeSettings,
}) {
assert(builder != null);
assert(useRootNavigator != null);
return showGeneralDialog(
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(CupertinoDialogRoute<T>(
builder: builder,
context: context,
barrierDismissible: barrierDismissible,
barrierLabel: CupertinoLocalizations.of(context).modalBarrierDismissLabel,
barrierLabel: barrierLabel,
barrierColor: CupertinoDynamicColor.resolve(_kModalBarrierColor, context),
settings: routeSettings,
));
}
/// A dialog route that shows an iOS-style dialog.
///
/// It is used internally by [showCupertinoDialog] or can be directly pushed
/// onto the [Navigator] stack to enable state restoration. See
/// [showCupertinoDialog] for a state restoration app example.
///
/// This function takes a `builder` which typically builds a [Dialog] widget.
/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
/// returned by the `builder` does not share a context with the location that
/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
/// custom [StatefulWidget] if the dialog needs to update dynamically.
///
/// The `context` argument is used to look up
/// [CupertinoLocalizations.modalBarrierDismissLabel], which provides the
/// modal with a localized accessibility label that will be used for the
/// modal's barrier. However, a custom `barrierLabel` can be passed in as well.
///
/// The `barrierDismissible` argument is used to indicate whether tapping on the
/// barrier will dismiss the dialog. It is `true` by default and cannot be `null`.
///
/// The `barrierColor` argument is used to specify the color of the modal
/// barrier that darkens everything below the dialog. If `null`, then
/// [CupertinoDynamicColor.resolve] is used to compute the modal color.
///
/// The `settings` argument define the settings for this route. See
/// [RouteSettings] for details.
///
/// See also:
///
/// * [showCupertinoDialog], which is a way to display
/// an iOS-style dialog.
/// * [showGeneralDialog], which allows for customization of the dialog popup.
/// * [showDialog], which displays a Material dialog.
class CupertinoDialogRoute<T> extends RawDialogRoute<T> {
/// A dialog route that shows an iOS-style dialog.
CupertinoDialogRoute({
required WidgetBuilder builder,
required BuildContext context,
bool barrierDismissible = true,
Color? barrierColor,
String? barrierLabel,
// This transition duration was eyeballed comparing with iOS
transitionDuration: const Duration(milliseconds: 250),
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return builder(context);
},
transitionBuilder: _buildCupertinoDialogTransitions,
useRootNavigator: useRootNavigator,
routeSettings: routeSettings,
);
Duration transitionDuration = const Duration(milliseconds: 250),
RouteTransitionsBuilder? transitionBuilder = _buildCupertinoDialogTransitions,
RouteSettings? settings,
}) : assert(barrierDismissible != null),
super(
pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation) {
return builder(context);
},
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel ?? CupertinoLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: barrierColor ?? CupertinoDynamicColor.resolve(_kModalBarrierColor, context),
transitionDuration: transitionDuration,
transitionBuilder: transitionBuilder,
settings: settings,
);
}
......@@ -950,6 +950,69 @@ Widget _buildMaterialDialogTransitions(BuildContext context, Animation<double> a
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
/// ### State Restoration in Dialogs
///
/// Using this method will not enable state restoration for the dialog. In order
/// to enable state restoration for a dialog, use [Navigator.restorablePush]
/// or [Navigator.restorablePushNamed] with [DialogRoute].
///
/// For more information about state restoration, see [RestorationManager].
///
/// {@tool sample --template=freeform}
///
/// This sample demonstrates how to create a restorable Material dialog. This is
/// accomplished by enabling state restoration by specifying
/// [MaterialApp.restorationScopeId] and using [Navigator.restorablePush] to
/// push [DialogRoute] when the button is tapped.
///
/// {@macro flutter.widgets.RestorationManager}
///
/// ```dart imports
/// import 'package:flutter/material.dart';
/// ```
///
/// ```dart
/// void main() {
/// runApp(MyApp());
/// }
///
/// class MyApp extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// restorationScopeId: 'app',
/// title: 'Restorable Routes Demo',
/// home: MyHomePage(),
/// );
/// }
/// }
///
/// class MyHomePage extends StatelessWidget {
/// static Route<Object?> _dialogBuilder(BuildContext context, Object? arguments) {
/// return DialogRoute<void>(
/// context: context,
/// builder: (BuildContext context) => const AlertDialog(title: Text('Material Alert!')),
/// );
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: Center(
/// child: OutlinedButton(
/// onPressed: () {
/// Navigator.of(context).restorablePush(_dialogBuilder);
/// },
/// child: const Text('Open Dialog'),
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// {@end-tool}
///
/// See also:
///
/// * [AlertDialog], for dialogs that have a row of buttons below a body.
......@@ -961,9 +1024,10 @@ Widget _buildMaterialDialogTransitions(BuildContext context, Animation<double> a
/// * <https://material.io/design/components/dialogs.html>
Future<T?> showDialog<T>({
required BuildContext context,
WidgetBuilder? builder,
required WidgetBuilder builder,
bool barrierDismissible = true,
Color? barrierColor,
Color? barrierColor = Colors.black54,
String? barrierLabel,
bool useSafeArea = true,
bool useRootNavigator = true,
RouteSettings? routeSettings,
......@@ -974,25 +1038,96 @@ Future<T?> showDialog<T>({
assert(useRootNavigator != null);
assert(debugCheckHasMaterialLocalizations(context));
final CapturedThemes themes = InheritedTheme.capture(from: context, to: Navigator.of(context, rootNavigator: useRootNavigator).context);
return showGeneralDialog(
final CapturedThemes themes = InheritedTheme.capture(
from: context,
to: Navigator.of(
context,
rootNavigator: useRootNavigator,
).context,
);
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(DialogRoute<T>(
context: context,
pageBuilder: (BuildContext buildContext, Animation<double> animation, Animation<double> secondaryAnimation) {
final Widget pageChild = Builder(builder: builder!);
Widget dialog = themes.wrap(pageChild);
if (useSafeArea) {
dialog = SafeArea(child: dialog);
}
return dialog;
},
builder: builder,
barrierColor: barrierColor,
barrierDismissible: barrierDismissible,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: barrierColor ?? Colors.black54,
transitionDuration: const Duration(milliseconds: 150),
transitionBuilder: _buildMaterialDialogTransitions,
useRootNavigator: useRootNavigator,
routeSettings: routeSettings,
);
barrierLabel: barrierLabel,
useSafeArea: useSafeArea,
settings: routeSettings,
themes: themes,
));
}
/// A dialog route with Material entrance and exit animations,
/// modal barrier color, and modal barrier behavior (dialog is dismissible
/// with a tap on the barrier).
///
/// It is used internally by [showDialog] or can be directly pushed
/// onto the [Navigator] stack to enable state restoration. See
/// [showDialog] for a state restoration app example.
///
/// This function takes a `builder` which typically builds a [Dialog] widget.
/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
/// returned by the `builder` does not share a context with the location that
/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
/// custom [StatefulWidget] if the dialog needs to update dynamically.
///
/// The `context` argument is used to look up
/// [MaterialLocalizations.modalBarrierDismissLabel], which provides the
/// modal with a localized accessibility label that will be used for the
/// modal's barrier. However, a custom `barrierLabel` can be passed in as well.
///
/// The `barrierDismissible` argument is used to indicate whether tapping on the
/// barrier will dismiss the dialog. It is `true` by default and cannot be `null`.
///
/// The `barrierColor` argument is used to specify the color of the modal
/// barrier that darkens everything below the dialog. If `null`, the default
/// color `Colors.black54` is used.
///
/// The `useSafeArea` argument is used to indicate if the dialog should only
/// display in 'safe' areas of the screen not used by the operating system
/// (see [SafeArea] for more details). It is `true` by default, which means
/// the dialog will not overlap operating system areas. If it is set to `false`
/// the dialog will only be constrained by the screen size. It can not be `null`.
///
/// The `settings` argument define the settings for this route. See
/// [RouteSettings] for details.
///
/// See also:
///
/// * [showDialog], which is a way to display a DialogRoute.
/// * [showGeneralDialog], which allows for customization of the dialog popup.
/// * [showCupertinoDialog], which displays an iOS-style dialog.
class DialogRoute<T> extends RawDialogRoute<T> {
/// A dialog route with Material entrance and exit animations,
/// modal barrier color, and modal barrier behavior (dialog is dismissible
/// with a tap on the barrier).
DialogRoute({
required BuildContext context,
required WidgetBuilder builder,
CapturedThemes? themes,
Color? barrierColor = Colors.black54,
bool barrierDismissible = true,
String? barrierLabel,
bool useSafeArea = true,
RouteSettings? settings,
}) : assert(barrierDismissible != null),
super(
pageBuilder: (BuildContext buildContext, Animation<double> animation, Animation<double> secondaryAnimation) {
final Widget pageChild = Builder(builder: builder);
Widget dialog = themes?.wrap(pageChild) ?? pageChild;
if (useSafeArea) {
dialog = SafeArea(child: dialog);
}
return dialog;
},
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
barrierLabel: barrierLabel ?? MaterialLocalizations.of(context).modalBarrierDismissLabel,
transitionDuration: const Duration(milliseconds: 150),
transitionBuilder: _buildMaterialDialogTransitions,
settings: settings,
);
}
double _paddingScaleFactor(double textScaleFactor) {
......
......@@ -115,6 +115,34 @@ typedef _BucketVisitor = void Function(RestorationBucket bucket);
/// fully re-compile your application (e.g. by re-executing `flutter run`) after
/// making a change.
///
/// ## Testing State Restoration
///
/// {@template flutter.widgets.RestorationManager}
/// To test state restoration on Android:
/// 1. Turn on "Don't keep activities", which destroys the Android activity
/// as soon as the user leaves it. This option should become available
/// when Developer Options are turned on for the device.
/// 2. Run the code sample on an Android device.
/// 3. Create some in-memory state in the app on the phone,
/// e.g. by navigating to a different screen.
/// 4. Background the Flutter app, then return to it. It will restart
/// and restore its state.
///
/// To test state restoration on iOS:
/// 1. Open `ios/Runner.xcworkspace/` in Xcode.
/// 2. (iOS 14+ only): Switch to build in profile or release mode, as
/// launching an app from the home screen is not supported in debug
/// mode.
/// 2. Press the Play button in Xcode to build and run the app.
/// 3. Create some in-memory state in the app on the phone,
/// e.g. by navigating to a different screen.
/// 4. Background the app on the phone, e.g. by going back to the home screen.
/// 5. Press the Stop button in Xcode to terminate the app while running in
/// the background.
/// 6. Open the app again on the phone (not via Xcode). It will restart
/// and restore its state.
/// {@endtemplate}
///
/// See also:
///
/// * [ServicesBinding.restorationManager], which holds the singleton instance
......
......@@ -1747,12 +1747,40 @@ abstract class RouteAware {
void didPushNext() { }
}
class _DialogRoute<T> extends PopupRoute<T> {
_DialogRoute({
/// A general dialog route which allows for customization of the dialog popup.
///
/// It is used internally by [showGeneralDialog] or can be directly pushed
/// onto the [Navigator] stack to enable state restoration. See
/// [showGeneralDialog] for a state restoration app example.
///
/// This function takes a `pageBuilder`, which typically builds a dialog.
/// Content below the dialog is dimmed with a [ModalBarrier]. The widget
/// returned by the `builder` does not share a context with the location that
/// `showDialog` is originally called from. Use a [StatefulBuilder] or a
/// custom [StatefulWidget] if the dialog needs to update dynamically.
///
/// The `barrierDismissible` argument is used to indicate whether tapping on the
/// barrier will dismiss the dialog. It is `true` by default and cannot be `null`.
///
/// The `barrierColor` argument is used to specify the color of the modal
/// barrier that darkens everything below the dialog. If `null`, the default
/// color `Colors.black54` is used.
///
/// The `settings` argument define the settings for this route. See
/// [RouteSettings] for details.
///
/// See also:
///
/// * [showGeneralDialog], which is a way to display a RawDialogRoute.
/// * [showDialog], which is a way to display a DialogRoute.
/// * [showCupertinoDialog], which displays an iOS-style dialog.
class RawDialogRoute<T> extends PopupRoute<T> {
/// A general dialog route which allows for customization of the dialog popup.
RawDialogRoute({
required RoutePageBuilder pageBuilder,
bool barrierDismissible = true,
String? barrierLabel,
Color? barrierColor = const Color(0x80000000),
String? barrierLabel,
Duration transitionDuration = const Duration(milliseconds: 200),
RouteTransitionsBuilder? transitionBuilder,
RouteSettings? settings,
......@@ -1858,6 +1886,73 @@ class _DialogRoute<T> extends PopupRoute<T> {
/// Returns a [Future] that resolves to the value (if any) that was passed to
/// [Navigator.pop] when the dialog was closed.
///
/// ### State Restoration in Dialogs
///
/// Using this method will not enable state restoration for the dialog. In order
/// to enable state restoration for a dialog, use [Navigator.restorablePush]
/// or [Navigator.restorablePushNamed] with [RawDialogRoute].
///
/// For more information about state restoration, see [RestorationManager].
///
/// {@tool sample --template=freeform}
///
/// This sample demonstrates how to create a restorable dialog. This is
/// accomplished by enabling state restoration by specifying
/// [WidgetsApp.restorationScopeId] and using [Navigator.restorablePush] to
/// push [RawDialogRoute] when the button is tapped.
///
/// {@macro flutter.widgets.RestorationManager}
///
/// ```dart imports
/// import 'package:flutter/material.dart';
/// ```
///
/// ```dart
/// void main() {
/// runApp(MyApp());
/// }
///
/// class MyApp extends StatelessWidget {
/// @override
/// Widget build(BuildContext context) {
/// return MaterialApp(
/// restorationScopeId: 'app',
/// home: MyHomePage(),
/// );
/// }
/// }
///
/// class MyHomePage extends StatelessWidget {
/// static Route<Object?> _dialogBuilder(BuildContext context, Object? arguments) {
/// return RawDialogRoute<void>(
/// pageBuilder: (
/// BuildContext context,
/// Animation<double> animation,
/// Animation<double> secondaryAnimation,
/// ) {
/// return const AlertDialog(title: Text('Alert!'));
/// },
/// );
/// }
///
/// @override
/// Widget build(BuildContext context) {
/// return Scaffold(
/// body: Center(
/// child: OutlinedButton(
/// onPressed: () {
/// Navigator.of(context).restorablePush(_dialogBuilder);
/// },
/// child: const Text('Open Dialog'),
/// ),
/// ),
/// );
/// }
/// }
/// ```
///
/// {@end-tool}
///
/// See also:
///
/// * [showDialog], which displays a Material-style dialog.
......@@ -1876,7 +1971,7 @@ Future<T?> showGeneralDialog<T extends Object?>({
assert(pageBuilder != null);
assert(useRootNavigator != null);
assert(!barrierDismissible || barrierLabel != null);
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(_DialogRoute<T>(
return Navigator.of(context, rootNavigator: useRootNavigator).push<T>(RawDialogRoute<T>(
pageBuilder: pageBuilder,
barrierDismissible: barrierDismissible,
barrierLabel: barrierLabel,
......
......@@ -1175,6 +1175,75 @@ void main() {
matchesGoldenFile('dialog_test.cupertino.default.png'),
);
});
testWidgets('showCupertinoDialog - custom barrierLabel', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
CupertinoApp(
home: Builder(
builder: (BuildContext context) {
return Center(
child: CupertinoButton(
child: const Text('X'),
onPressed: () {
showCupertinoDialog<void>(
context: context,
barrierLabel: 'Custom label',
builder: (BuildContext context) {
return const CupertinoAlertDialog(
title: Text('Title'),
content: Text('Content'),
actions: <Widget>[
CupertinoDialogAction(child: Text('Yes')),
CupertinoDialogAction(child: Text('No')),
],
);
},
);
},
),
);
},
),
),
);
expect(semantics, isNot(includesNodeWith(
label: 'Custom label',
flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
)));
});
testWidgets('CupertinoDialogRoute is state restorable', (WidgetTester tester) async {
await tester.pumpWidget(
CupertinoApp(
restorationScopeId: 'app',
home: _RestorableDialogTestWidget(),
),
);
expect(find.byType(CupertinoAlertDialog), findsNothing);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(find.byType(CupertinoAlertDialog), findsOneWidget);
final TestRestorationData restorationData = await tester.getRestorationData();
await tester.restartAndRestore();
expect(find.byType(CupertinoAlertDialog), findsOneWidget);
// Tap on the barrier.
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
expect(find.byType(CupertinoAlertDialog), findsNothing);
await tester.restoreFrom(restorationData);
expect(find.byType(CupertinoAlertDialog), findsOneWidget);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
}
RenderBox findActionButtonRenderBoxByTitle(WidgetTester tester, String title) {
......@@ -1234,3 +1303,37 @@ Widget createAppWithCenteredButton(Widget child) {
)
);
}
class _RestorableDialogTestWidget extends StatelessWidget{
static Route<Object?> _dialogBuilder(BuildContext context, Object? arguments) {
return CupertinoDialogRoute<void>(
context: context,
builder: (BuildContext context) {
return const CupertinoAlertDialog(
title: Text('Title'),
content: Text('Content'),
actions: <Widget>[
CupertinoDialogAction(child: Text('Yes')),
CupertinoDialogAction(child: Text('No')),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(
middle: Text('Home'),
),
child: Center(child: CupertinoButton(
onPressed: () {
Navigator.of(context).restorablePush(_dialogBuilder);
},
child: const Text('X'),
)),
);
}
}
......@@ -1734,7 +1734,7 @@ class DialogObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route.toString().contains('_DialogRoute')) {
if (route is CupertinoDialogRoute) {
dialogCount++;
}
super.didPush(route, previousRoute);
......
......@@ -744,7 +744,7 @@ class AboutDialogObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route.toString().contains('_DialogRoute')) {
if (route is DialogRoute) {
dialogCount++;
}
super.didPush(route, previousRoute);
......
......@@ -1095,7 +1095,7 @@ class _DatePickerObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route.toString().contains('_DialogRoute')) {
if (route is DialogRoute) {
datePickerCount++;
}
super.didPush(route, previousRoute);
......
......@@ -217,29 +217,6 @@ void main() {
expect(materialWidget.shape, customBorder);
});
testWidgets('showDialog builder must be defined', (WidgetTester tester) async {
late BuildContext currentBuildContext;
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Center(
child: Builder(
builder: (BuildContext context) {
currentBuildContext = context;
return Container();
}
),
),
),
),
);
expect(
() => showDialog<void>(context: currentBuildContext),
throwsAssertionError,
);
});
testWidgets('Simple dialog control test', (WidgetTester tester) async {
await tester.pumpWidget(
const MaterialApp(
......@@ -1856,6 +1833,98 @@ void main() {
await tester.pumpAndSettle();
expect(currentRouteSetting.name, '/');
});
testWidgets('showDialog - custom barrierLabel', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: TargetPlatform.iOS),
home: Material(
child: Builder(
builder: (BuildContext context) {
return Center(
child: ElevatedButton(
child: const Text('X'),
onPressed: () {
showDialog<void>(
context: context,
barrierLabel: 'Custom label',
builder: (BuildContext context) {
return const AlertDialog(
title: Text('Title'),
content: Text('Y'),
actions: <Widget>[],
);
},
);
},
),
);
},
),
),
),
);
expect(semantics, isNot(includesNodeWith(
label: 'Custom label',
flags: <SemanticsFlag>[SemanticsFlag.namesRoute],
)));
});
testWidgets('DialogRoute is state restorable', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
restorationScopeId: 'app',
home: _RestorableDialogTestWidget(),
),
);
expect(find.byType(AlertDialog), findsNothing);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
final TestRestorationData restorationData = await tester.getRestorationData();
await tester.restartAndRestore();
expect(find.byType(AlertDialog), findsOneWidget);
// Tap on the barrier.
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
await tester.restoreFrom(restorationData);
expect(find.byType(AlertDialog), findsOneWidget);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
}
class _RestorableDialogTestWidget extends StatelessWidget {
static Route<Object?> _materialDialogBuilder(BuildContext context, Object? arguments) {
return DialogRoute<void>(
context: context,
builder: (BuildContext context) => const AlertDialog(title: Text('Material Alert!')),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: OutlinedButton(
onPressed: () {
Navigator.of(context).restorablePush(_materialDialogBuilder);
},
child: const Text('X'),
),
),
);
}
}
class DialogObserver extends NavigatorObserver {
......@@ -1863,7 +1932,7 @@ class DialogObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route.toString().contains('_DialogRoute')) {
if (route is DialogRoute) {
dialogCount++;
}
super.didPush(route, previousRoute);
......
......@@ -958,7 +958,7 @@ class PickerObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route.toString().contains('_DialogRoute')) {
if (route is DialogRoute) {
pickerCount++;
}
super.didPush(route, previousRoute);
......
......@@ -1748,6 +1748,36 @@ void main() {
expect(parentRoute, isNotNull);
expect(parentRoute, isA<MaterialPageRoute<void>>());
});
testWidgets('RawDialogRoute is state restorable', (WidgetTester tester) async {
await tester.pumpWidget(
MaterialApp(
restorationScopeId: 'app',
home: _RestorableDialogTestWidget(),
),
);
expect(find.byType(AlertDialog), findsNothing);
await tester.tap(find.text('X'));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
final TestRestorationData restorationData = await tester.getRestorationData();
await tester.restartAndRestore();
expect(find.byType(AlertDialog), findsOneWidget);
// Tap on the barrier.
await tester.tapAt(const Offset(10.0, 10.0));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
await tester.restoreFrom(restorationData);
expect(find.byType(AlertDialog), findsOneWidget);
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/33615
}
double _getOpacity(GlobalKey key, WidgetTester tester) {
......@@ -1823,8 +1853,8 @@ class DialogObserver extends NavigatorObserver {
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route.toString().contains('_DialogRoute')) {
dialogRoutes.add(route as ModalRoute<dynamic>);
if (route is RawDialogRoute) {
dialogRoutes.add(route);
dialogCount++;
}
super.didPush(route, previousRoute);
......@@ -1832,7 +1862,7 @@ class DialogObserver extends NavigatorObserver {
@override
void didPop(Route<dynamic> route, Route<dynamic>? previousRoute) {
if (route.toString().contains('_DialogRoute')) {
if (route is RawDialogRoute) {
dialogRoutes.removeLast();
dialogCount--;
}
......@@ -1951,3 +1981,31 @@ Widget buildNavigator({
),
);
}
class _RestorableDialogTestWidget extends StatelessWidget {
static Route<Object?> _dialogBuilder(BuildContext context, Object? arguments) {
return RawDialogRoute<void>(
pageBuilder: (
BuildContext context,
Animation<double> animation,
Animation<double> secondaryAnimation,
) {
return const AlertDialog(title: Text('Alert!'));
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: OutlinedButton(
onPressed: () {
Navigator.of(context).restorablePush(_dialogBuilder);
},
child: const Text('X'),
),
),
);
}
}
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