Skip to content

Commit bd2617e

Browse files
Adaptive alert dialog (#124336)
Fixes #102811. Adds an adaptive constructor to AlertDialog, along with the adaptive function showAdaptiveDialog. <img width="357" alt="Screenshot 2023-04-06 at 10 40 18 AM" src="https://user-images.githubusercontent.com/58190796/230455412-31100922-cfc5-4252-b8c6-6f076353f29e.png"> <img width="350" alt="Screenshot 2023-04-06 at 10 42 50 AM" src="https://user-images.githubusercontent.com/58190796/230455454-363dd37e-c44e-4aca-b6a0-cfa1d959f606.png">
1 parent c05bc40 commit bd2617e

File tree

4 files changed

+391
-1
lines changed

4 files changed

+391
-1
lines changed
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/cupertino.dart';
6+
import 'package:flutter/material.dart';
7+
8+
/// Flutter code sample for [AlertDialog].
9+
10+
void main() => runApp(const AdaptiveAlertDialogApp());
11+
12+
class AdaptiveAlertDialogApp extends StatelessWidget {
13+
const AdaptiveAlertDialogApp({super.key});
14+
15+
@override
16+
Widget build(BuildContext context) {
17+
return MaterialApp(
18+
// Try this: set the platform to TargetPlatform.android and see the difference
19+
theme: ThemeData(platform: TargetPlatform.iOS, useMaterial3: true),
20+
home: Scaffold(
21+
appBar: AppBar(title: const Text('AlertDialog Sample')),
22+
body: const Center(
23+
child: AdaptiveDialogExample(),
24+
),
25+
),
26+
);
27+
}
28+
}
29+
30+
class AdaptiveDialogExample extends StatelessWidget {
31+
const AdaptiveDialogExample({super.key});
32+
33+
Widget adaptiveAction({
34+
required BuildContext context,
35+
required VoidCallback onPressed,
36+
required Widget child
37+
}) {
38+
final ThemeData theme = Theme.of(context);
39+
switch (theme.platform) {
40+
case TargetPlatform.android:
41+
case TargetPlatform.fuchsia:
42+
case TargetPlatform.linux:
43+
case TargetPlatform.windows:
44+
return TextButton(onPressed: onPressed, child: child);
45+
case TargetPlatform.iOS:
46+
case TargetPlatform.macOS:
47+
return CupertinoDialogAction(onPressed: onPressed, child: child);
48+
}
49+
}
50+
51+
@override
52+
Widget build(BuildContext context) {
53+
return TextButton(
54+
onPressed: () => showAdaptiveDialog<String>(
55+
context: context,
56+
builder: (BuildContext context) => AlertDialog.adaptive(
57+
title: const Text('AlertDialog Title'),
58+
content: const Text('AlertDialog description'),
59+
actions: <Widget>[
60+
adaptiveAction(
61+
context: context,
62+
onPressed: () => Navigator.pop(context, 'Cancel'),
63+
child: const Text('Cancel'),
64+
),
65+
adaptiveAction(
66+
context: context,
67+
onPressed: () => Navigator.pop(context, 'OK'),
68+
child: const Text('OK'),
69+
),
70+
],
71+
),
72+
),
73+
child: const Text('Show Dialog'),
74+
);
75+
}
76+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
// Copyright 2014 The Flutter Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:flutter/material.dart';
6+
import 'package:flutter_api_samples/material/dialog/adaptive_alert_dialog.0.dart' as example;
7+
import 'package:flutter_test/flutter_test.dart';
8+
9+
void main() {
10+
testWidgets('Show Adaptive Alert dialog', (WidgetTester tester) async {
11+
const String dialogTitle = 'AlertDialog Title';
12+
await tester.pumpWidget(
13+
const MaterialApp(
14+
home: Scaffold(
15+
body: example.AdaptiveAlertDialogApp(),
16+
),
17+
),
18+
);
19+
20+
expect(find.text(dialogTitle), findsNothing);
21+
22+
await tester.tap(find.widgetWithText(TextButton, 'Show Dialog'));
23+
await tester.pumpAndSettle();
24+
expect(find.text(dialogTitle), findsOneWidget);
25+
26+
await tester.tap(find.text('OK'));
27+
await tester.pumpAndSettle();
28+
expect(find.text(dialogTitle), findsNothing);
29+
});
30+
}

packages/flutter/lib/src/material/dialog.dart

Lines changed: 182 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
import 'dart:ui';
66

7+
import 'package:flutter/cupertino.dart';
78
import 'package:flutter/foundation.dart' show clampDouble;
8-
import 'package:flutter/widgets.dart';
99

1010
import 'color_scheme.dart';
1111
import 'colors.dart';
@@ -395,6 +395,69 @@ class AlertDialog extends StatelessWidget {
395395
this.scrollable = false,
396396
});
397397

398+
/// Creates an adaptive [AlertDialog] based on whether the target platform is
399+
/// iOS or macOS, following Material design's
400+
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
401+
///
402+
/// On iOS and macOS, this constructor creates a [CupertinoAlertDialog]. On
403+
/// other platforms, this creates a Material design [AlertDialog].
404+
///
405+
/// Typically passed as a child of [showAdaptiveDialog], which will display
406+
/// the alert differently based on platform.
407+
///
408+
/// If a [CupertinoAlertDialog] is created only these parameters are used:
409+
/// [title], [content], [actions], [scrollController],
410+
/// [actionScrollController], [insetAnimationDuration], and
411+
/// [insetAnimationCurve]. If a material [AlertDialog] is created,
412+
/// [scrollController], [actionScrollController], [insetAnimationDuration],
413+
/// and [insetAnimationCurve] are ignored.
414+
///
415+
/// The target platform is based on the current [Theme]: [ThemeData.platform].
416+
///
417+
/// {@tool dartpad}
418+
/// This demo shows a [TextButton] which when pressed, calls [showAdaptiveDialog].
419+
/// When called, this method displays an adaptive dialog above the current
420+
/// contents of the app, with different behaviors depending on target platform.
421+
///
422+
/// [CupertinoDialogAction] is conditionally used as the child to show more
423+
/// platform specific design.
424+
///
425+
/// ** See code in examples/api/lib/material/dialog/adaptive_alert_dialog.0.dart **
426+
/// {@end-tool}
427+
const factory AlertDialog.adaptive({
428+
Key? key,
429+
Widget? icon,
430+
EdgeInsetsGeometry? iconPadding,
431+
Color? iconColor,
432+
Widget? title,
433+
EdgeInsetsGeometry? titlePadding,
434+
TextStyle? titleTextStyle,
435+
Widget? content,
436+
EdgeInsetsGeometry? contentPadding,
437+
TextStyle? contentTextStyle,
438+
List<Widget>? actions,
439+
EdgeInsetsGeometry? actionsPadding,
440+
MainAxisAlignment? actionsAlignment,
441+
OverflowBarAlignment? actionsOverflowAlignment,
442+
VerticalDirection? actionsOverflowDirection,
443+
double? actionsOverflowButtonSpacing,
444+
EdgeInsetsGeometry? buttonPadding,
445+
Color? backgroundColor,
446+
double? elevation,
447+
Color? shadowColor,
448+
Color? surfaceTintColor,
449+
String? semanticLabel,
450+
EdgeInsets insetPadding,
451+
Clip clipBehavior,
452+
ShapeBorder? shape,
453+
AlignmentGeometry? alignment,
454+
bool scrollable,
455+
ScrollController? scrollController,
456+
ScrollController? actionScrollController,
457+
Duration insetAnimationDuration,
458+
Curve insetAnimationCurve,
459+
}) = _AdaptiveAlertDialog;
460+
398461
/// An optional icon to display at the top of the dialog.
399462
///
400463
/// Typically, an [Icon] widget. Providing an icon centers the [title]'s text.
@@ -638,6 +701,7 @@ class AlertDialog extends StatelessWidget {
638701
Widget build(BuildContext context) {
639702
assert(debugCheckHasMaterialLocalizations(context));
640703
final ThemeData theme = Theme.of(context);
704+
641705
final DialogTheme dialogTheme = DialogTheme.of(context);
642706
final DialogTheme defaults = theme.useMaterial3 ? _DialogDefaultsM3(context) : _DialogDefaultsM2(context);
643707

@@ -823,6 +887,71 @@ class AlertDialog extends StatelessWidget {
823887
}
824888
}
825889

890+
class _AdaptiveAlertDialog extends AlertDialog {
891+
const _AdaptiveAlertDialog({
892+
super.key,
893+
super.icon,
894+
super.iconPadding,
895+
super.iconColor,
896+
super.title,
897+
super.titlePadding,
898+
super.titleTextStyle,
899+
super.content,
900+
super.contentPadding,
901+
super.contentTextStyle,
902+
super.actions,
903+
super.actionsPadding,
904+
super.actionsAlignment,
905+
super.actionsOverflowAlignment,
906+
super.actionsOverflowDirection,
907+
super.actionsOverflowButtonSpacing,
908+
super.buttonPadding,
909+
super.backgroundColor,
910+
super.elevation,
911+
super.shadowColor,
912+
super.surfaceTintColor,
913+
super.semanticLabel,
914+
super.insetPadding = _defaultInsetPadding,
915+
super.clipBehavior = Clip.none,
916+
super.shape,
917+
super.alignment,
918+
super.scrollable = false,
919+
this.scrollController,
920+
this.actionScrollController,
921+
this.insetAnimationDuration = const Duration(milliseconds: 100),
922+
this.insetAnimationCurve = Curves.decelerate,
923+
});
924+
925+
final ScrollController? scrollController;
926+
final ScrollController? actionScrollController;
927+
final Duration insetAnimationDuration;
928+
final Curve insetAnimationCurve;
929+
930+
@override
931+
Widget build(BuildContext context) {
932+
final ThemeData theme = Theme.of(context);
933+
switch(theme.platform) {
934+
case TargetPlatform.android:
935+
case TargetPlatform.fuchsia:
936+
case TargetPlatform.linux:
937+
case TargetPlatform.windows:
938+
break;
939+
case TargetPlatform.iOS:
940+
case TargetPlatform.macOS:
941+
return CupertinoAlertDialog(
942+
title: title,
943+
content: content,
944+
actions: actions ?? <Widget>[],
945+
scrollController: scrollController,
946+
actionScrollController: actionScrollController,
947+
insetAnimationDuration: insetAnimationDuration,
948+
insetAnimationCurve: insetAnimationCurve,
949+
);
950+
}
951+
return super.build(context);
952+
}
953+
}
954+
826955
/// An option used in a [SimpleDialog].
827956
///
828957
/// A simple dialog offers the user a choice between several options. This
@@ -1308,6 +1437,58 @@ Future<T?> showDialog<T>({
13081437
));
13091438
}
13101439

1440+
/// Displays either a Material or Cupertino dialog depending on platform.
1441+
///
1442+
/// On most platforms this function will act the same as [showDialog], except
1443+
/// for iOS and macOS, in which case it will act the same as
1444+
/// [showCupertinoDialog].
1445+
///
1446+
/// On Cupertino platforms, [barrierColor], [useSafeArea], and
1447+
/// [traversalEdgeBehavior] are ignored.
1448+
Future<T?> showAdaptiveDialog<T>({
1449+
required BuildContext context,
1450+
required WidgetBuilder builder,
1451+
bool? barrierDismissible,
1452+
Color? barrierColor = Colors.black54,
1453+
String? barrierLabel,
1454+
bool useSafeArea = true,
1455+
bool useRootNavigator = true,
1456+
RouteSettings? routeSettings,
1457+
Offset? anchorPoint,
1458+
TraversalEdgeBehavior? traversalEdgeBehavior,
1459+
}) {
1460+
final ThemeData theme = Theme.of(context);
1461+
switch (theme.platform) {
1462+
case TargetPlatform.android:
1463+
case TargetPlatform.fuchsia:
1464+
case TargetPlatform.linux:
1465+
case TargetPlatform.windows:
1466+
return showDialog<T>(
1467+
context: context,
1468+
builder: builder,
1469+
barrierDismissible: barrierDismissible ?? true,
1470+
barrierColor: barrierColor,
1471+
barrierLabel: barrierLabel,
1472+
useSafeArea: useSafeArea,
1473+
useRootNavigator: useRootNavigator,
1474+
routeSettings: routeSettings,
1475+
anchorPoint: anchorPoint,
1476+
traversalEdgeBehavior: traversalEdgeBehavior,
1477+
);
1478+
case TargetPlatform.iOS:
1479+
case TargetPlatform.macOS:
1480+
return showCupertinoDialog<T>(
1481+
context: context,
1482+
builder: builder,
1483+
barrierDismissible: barrierDismissible ?? false,
1484+
barrierLabel: barrierLabel,
1485+
useRootNavigator: useRootNavigator,
1486+
anchorPoint: anchorPoint,
1487+
routeSettings: routeSettings,
1488+
);
1489+
}
1490+
}
1491+
13111492
bool _debugIsActive(BuildContext context) {
13121493
if (context is Element && !context.debugIsActive) {
13131494
throw FlutterError.fromParts(<DiagnosticsNode>[

0 commit comments

Comments
 (0)