[#155269] Platform adaptive showAboutDialog and AboutDialog feature c… (#155377)

This PR introduces a new `showAdaptiveAboutDialog` function, ensuring that the About dialog matches the platform’s design (Material for Android, Fuchsia, Linux, Windows, and, Cupertino for iOS & macOS), providing a more consistent user experience across platforms.

Fixes #155269 

- [] I followed the [breaking change policy] and added [Data Driven Fixes] where supported.
This commit is contained in:
Jiten Patel 2024-10-22 23:02:05 +05:30 committed by GitHub
parent eb5ef69d6b
commit 5fa5a64842
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 327 additions and 0 deletions

View File

@ -9,6 +9,7 @@ library;
import 'dart:developer' show Flow, Timeline;
import 'dart:io' show Platform;
import 'package:flutter/cupertino.dart' show CupertinoDialogAction;
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart' hide Flow;
@ -211,6 +212,62 @@ void showAboutDialog({
);
}
/// Displays either a Material or Cupertino [AboutDialog] depending on platform,
/// which describes the application and provides a button to show licenses
/// for software used by the application.
///
/// The arguments correspond to the properties on [AboutDialog].
///
/// If the application has a [Drawer], consider using [AboutListTile] instead
/// of calling this directly.
///
/// If you do not need an about box in your application, you should at least
/// provide an affordance to call [showLicensePage].
///
/// The licenses shown on the [LicensePage] are those returned by the
/// [LicenseRegistry] API, which can be used to add more licenses to the list.
///
/// On most platforms this function will act the same as [showDialog], except
/// for iOS and macOS, in which case it will act the same as
/// [showCupertinoDialog].
///
/// The [context], [barrierDismissible], [barrierColor], [barrierLabel],
/// [useRootNavigator], [routeSettings] and [anchorPoint] arguments are
/// passed to [showAdaptiveDialog], the documentation for which discusses how it is used.
void showAdaptiveAboutDialog({
required BuildContext context,
String? applicationName,
String? applicationVersion,
Widget? applicationIcon,
String? applicationLegalese,
List<Widget>? children,
bool barrierDismissible = true,
Color? barrierColor,
String? barrierLabel,
bool useRootNavigator = true,
RouteSettings? routeSettings,
Offset? anchorPoint,
}) {
showAdaptiveDialog<void>(
context: context,
barrierDismissible: barrierDismissible,
barrierColor: barrierColor,
barrierLabel: barrierLabel,
useRootNavigator: useRootNavigator,
builder: (BuildContext context) {
return AboutDialog.adaptive(
applicationName: applicationName,
applicationVersion: applicationVersion,
applicationIcon: applicationIcon,
applicationLegalese: applicationLegalese,
children: children,
);
},
routeSettings: routeSettings,
anchorPoint: anchorPoint,
);
}
/// Displays a [LicensePage], which shows licenses for software used by the
/// application.
///
@ -282,6 +339,28 @@ class AboutDialog extends StatelessWidget {
this.children,
});
/// Creates an adaptive [AboutDialog] based on whether the target platform is
/// iOS or macOS, following Material design's
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
///
/// Typically passed as a child of [showAdaptiveAboutDialog], which will display
/// the [AboutDialog] differently based on platform.
///
/// This constructor offers the same customization options as the default
/// [AboutDialog] constructor, allowing you to specify the application's
/// [applicationName], [applicationVersion], [applicationIcon],
/// [applicationLegalese], and additional [children] that appear in the dialog.
///
/// The target platform is based on the current [Theme]: [ThemeData.platform].
const factory AboutDialog.adaptive({
Key? key,
String? applicationName,
String? applicationVersion,
Widget? applicationIcon,
String? applicationLegalese,
List<Widget>? children,
}) = _AdaptiveAboutDialog;
/// The name of the application.
///
/// Defaults to the value of [Title.title], if a [Title] widget can be found.
@ -384,6 +463,119 @@ class AboutDialog extends StatelessWidget {
}
}
class _AdaptiveAboutDialog extends AboutDialog {
const _AdaptiveAboutDialog({
super.key,
super.applicationName,
super.applicationVersion,
super.applicationIcon,
super.applicationLegalese,
super.children,
});
List<Widget>? _actions(BuildContext context) {
final ThemeData themeData = Theme.of(context);
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
switch (themeData.platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
return <Widget>[
CupertinoDialogAction(
child: Text(themeData.useMaterial3
? localizations.viewLicensesButtonLabel
: localizations.viewLicensesButtonLabel.toUpperCase()),
onPressed: () {
showLicensePage(
context: context,
applicationName: applicationName,
applicationVersion: applicationVersion,
applicationIcon: applicationIcon,
applicationLegalese: applicationLegalese,
);
},
),
CupertinoDialogAction(
child: Text(themeData.useMaterial3
? localizations.closeButtonLabel
: localizations.closeButtonLabel.toUpperCase()),
onPressed: () {
Navigator.pop(context);
},
),
];
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
return <Widget>[
TextButton(
child: Text(themeData.useMaterial3
? localizations.viewLicensesButtonLabel
: localizations.viewLicensesButtonLabel.toUpperCase()),
onPressed: () {
showLicensePage(
context: context,
applicationName: applicationName,
applicationVersion: applicationVersion,
applicationIcon: applicationIcon,
applicationLegalese: applicationLegalese,
);
},
),
TextButton(
child: Text(themeData.useMaterial3
? localizations.closeButtonLabel
: localizations.closeButtonLabel.toUpperCase()),
onPressed: () {
Navigator.pop(context);
},
),
];
}
}
@override
Widget build(BuildContext context) {
super.build(context);
final String name = applicationName ?? _defaultApplicationName(context);
final String version = applicationVersion ?? _defaultApplicationVersion(context);
final Widget? icon = applicationIcon ?? _defaultApplicationIcon(context);
final ThemeData themeData = Theme.of(context);
final List<Widget>? actions = _actions(context);
return AlertDialog.adaptive(
content: ListBody(
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
if (icon != null) IconTheme(data: themeData.iconTheme, child: icon),
Expanded(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: ListBody(
children: <Widget>[
Text(name, style: themeData.textTheme.headlineSmall),
Text(version, style: themeData.textTheme.bodyMedium),
const SizedBox(height: _textVerticalSeparation),
Text(applicationLegalese ?? '', style: themeData.textTheme.bodySmall),
],
),
),
),
],
),
...?children,
],
),
actions: actions,
scrollable: true,
);
}
}
/// A page that shows licenses for software used by the application.
///
/// To show a [LicensePage], use [showLicensePage].

View File

@ -1841,6 +1841,141 @@ void main() {
expect(renderParagraph.text.style!.color, theme.primaryTextTheme.titleLarge!.color);
});
});
testWidgets('Adaptive AboutDialog shows correct widget on each platform',(WidgetTester tester) async {
for (final TargetPlatform platform in <TargetPlatform>[TargetPlatform.iOS, TargetPlatform.macOS]) {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: platform),
home: const Material(
child: Center(
child: ElevatedButton(
onPressed: null,
child: Text('Go'),
),
),
),
),
);
final BuildContext context = tester.element(find.text('Go'));
showAdaptiveAboutDialog(
context: context,
applicationIcon: const Icon(Icons.abc),
applicationName: 'Test',
applicationVersion: '1.0.0',
applicationLegalese: 'Application Legalese',
children: <Widget>[
const Text('Test1'),
],
);
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byType(CupertinoDialogAction), findsWidgets);
}
for (final TargetPlatform platform in <TargetPlatform>[
TargetPlatform.android,
TargetPlatform.fuchsia,
TargetPlatform.linux,
TargetPlatform.windows,
]) {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: platform),
home: const Material(
child: Center(
child: ElevatedButton(
onPressed: null,
child: Text('Go'),
),
),
),
),
);
final BuildContext context = tester.element(find.text('Go'));
showAboutDialog(
context: context,
applicationIcon: const Icon(Icons.abc),
applicationName: 'Test',
applicationVersion: '1.0.0',
);
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byType(CupertinoDialogAction), findsNothing);
}
});
testWidgets('Adaptive AboutDialog closes correctly on each platform', (WidgetTester tester) async {
for (final TargetPlatform platform in <TargetPlatform>[TargetPlatform.iOS, TargetPlatform.macOS]) {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: platform),
home: const Material(
child: Center(
child: ElevatedButton(
onPressed: null,
child: Text('Go'),
),
),
),
),
);
final BuildContext context = tester.element(find.text('Go'));
showAdaptiveAboutDialog(
context: context,
applicationName: 'Test',
applicationVersion: '1.0.0',
);
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byType(CupertinoDialogAction), findsWidgets);
await tester.tap(find.text('Close'));
await tester.pumpAndSettle();
expect(find.byType(CupertinoAlertDialog), findsNothing);
}
for (final TargetPlatform platform in <TargetPlatform>[
TargetPlatform.android,
TargetPlatform.fuchsia,
TargetPlatform.linux,
TargetPlatform.windows,
]) {
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(platform: platform),
home: const Material(
child: Center(
child: ElevatedButton(
onPressed: null,
child: Text('Go'),
),
),
),
),
);
final BuildContext context = tester.element(find.text('Go'));
showAdaptiveAboutDialog(
context: context,
applicationName: 'Test',
applicationVersion: '1.0.0',
);
await tester.pumpAndSettle(const Duration(seconds: 1));
expect(find.byType(TextButton), findsWidgets);
await tester.tap(find.text('Close'));
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsNothing);
}});
}
class FakeLicenseEntry extends LicenseEntry {