Expose DialogRoutes for state restoration support (#73829)
This commit is contained in:
parent
ea3aa674da
commit
5801f0e5fe
@ -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),
|
||||
// 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,
|
||||
);
|
||||
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
|
||||
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(
|
||||
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) {
|
||||
|
@ -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'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user