Expose DialogRoutes for state restoration support (#73829)
diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart
index ac988d4..94b97f2 100644
--- a/packages/flutter/lib/src/cupertino/route.dart
+++ b/packages/flutter/lib/src/cupertino/route.dart
@@ -1125,6 +1125,78 @@
/// 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 @@
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,
+ );
}
diff --git a/packages/flutter/lib/src/material/dialog.dart b/packages/flutter/lib/src/material/dialog.dart
index 2f931a5..e91ad80 100644
--- a/packages/flutter/lib/src/material/dialog.dart
+++ b/packages/flutter/lib/src/material/dialog.dart
@@ -950,6 +950,69 @@
/// 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 @@
/// * <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 @@
assert(useRootNavigator != null);
assert(debugCheckHasMaterialLocalizations(context));
- final CapturedThemes themes = InheritedTheme.capture(from: context, to: Navigator.of(context, rootNavigator: useRootNavigator).context);
- return showGeneralDialog(
- 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;
- },
- barrierDismissible: barrierDismissible,
- barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
- barrierColor: barrierColor ?? Colors.black54,
- transitionDuration: const Duration(milliseconds: 150),
- transitionBuilder: _buildMaterialDialogTransitions,
- useRootNavigator: useRootNavigator,
- routeSettings: routeSettings,
+ 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,
+ builder: builder,
+ barrierColor: barrierColor,
+ barrierDismissible: barrierDismissible,
+ 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) {
diff --git a/packages/flutter/lib/src/services/restoration.dart b/packages/flutter/lib/src/services/restoration.dart
index d623aa1..97b991e 100644
--- a/packages/flutter/lib/src/services/restoration.dart
+++ b/packages/flutter/lib/src/services/restoration.dart
@@ -115,6 +115,34 @@
/// 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
diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart
index cc77214..42b8a1d 100644
--- a/packages/flutter/lib/src/widgets/routes.dart
+++ b/packages/flutter/lib/src/widgets/routes.dart
@@ -1747,12 +1747,40 @@
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 @@
/// 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 @@
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,
diff --git a/packages/flutter/test/cupertino/dialog_test.dart b/packages/flutter/test/cupertino/dialog_test.dart
index 5a29b57..1fd014d 100644
--- a/packages/flutter/test/cupertino/dialog_test.dart
+++ b/packages/flutter/test/cupertino/dialog_test.dart
@@ -1175,6 +1175,75 @@
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 @@
)
);
}
+
+
+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'),
+ )),
+ );
+ }
+}
diff --git a/packages/flutter/test/cupertino/route_test.dart b/packages/flutter/test/cupertino/route_test.dart
index 5cbf1f9..6d94d8a 100644
--- a/packages/flutter/test/cupertino/route_test.dart
+++ b/packages/flutter/test/cupertino/route_test.dart
@@ -1734,7 +1734,7 @@
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
- if (route.toString().contains('_DialogRoute')) {
+ if (route is CupertinoDialogRoute) {
dialogCount++;
}
super.didPush(route, previousRoute);
diff --git a/packages/flutter/test/material/about_test.dart b/packages/flutter/test/material/about_test.dart
index 40aee8e..f15b7c1 100644
--- a/packages/flutter/test/material/about_test.dart
+++ b/packages/flutter/test/material/about_test.dart
@@ -744,7 +744,7 @@
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
- if (route.toString().contains('_DialogRoute')) {
+ if (route is DialogRoute) {
dialogCount++;
}
super.didPush(route, previousRoute);
diff --git a/packages/flutter/test/material/date_picker_test.dart b/packages/flutter/test/material/date_picker_test.dart
index fcd0107..ce7b815 100644
--- a/packages/flutter/test/material/date_picker_test.dart
+++ b/packages/flutter/test/material/date_picker_test.dart
@@ -1095,7 +1095,7 @@
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
- if (route.toString().contains('_DialogRoute')) {
+ if (route is DialogRoute) {
datePickerCount++;
}
super.didPush(route, previousRoute);
diff --git a/packages/flutter/test/material/dialog_test.dart b/packages/flutter/test/material/dialog_test.dart
index fdf64ec..d0f8a1d 100644
--- a/packages/flutter/test/material/dialog_test.dart
+++ b/packages/flutter/test/material/dialog_test.dart
@@ -217,29 +217,6 @@
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 @@
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 @@
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
- if (route.toString().contains('_DialogRoute')) {
+ if (route is DialogRoute) {
dialogCount++;
}
super.didPush(route, previousRoute);
diff --git a/packages/flutter/test/material/time_picker_test.dart b/packages/flutter/test/material/time_picker_test.dart
index 27c0718..d60375a 100644
--- a/packages/flutter/test/material/time_picker_test.dart
+++ b/packages/flutter/test/material/time_picker_test.dart
@@ -958,7 +958,7 @@
@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
- if (route.toString().contains('_DialogRoute')) {
+ if (route is DialogRoute) {
pickerCount++;
}
super.didPush(route, previousRoute);
diff --git a/packages/flutter/test/widgets/routes_test.dart b/packages/flutter/test/widgets/routes_test.dart
index 3c63c73..2e3fb09 100644
--- a/packages/flutter/test/widgets/routes_test.dart
+++ b/packages/flutter/test/widgets/routes_test.dart
@@ -1748,6 +1748,36 @@
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 @@
@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 @@
@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 @@
),
);
}
+
+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'),
+ ),
+ ),
+ );
+ }
+}