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:
parent
eb5ef69d6b
commit
5fa5a64842
@ -9,6 +9,7 @@ library;
|
|||||||
import 'dart:developer' show Flow, Timeline;
|
import 'dart:developer' show Flow, Timeline;
|
||||||
import 'dart:io' show Platform;
|
import 'dart:io' show Platform;
|
||||||
|
|
||||||
|
import 'package:flutter/cupertino.dart' show CupertinoDialogAction;
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/scheduler.dart';
|
import 'package:flutter/scheduler.dart';
|
||||||
import 'package:flutter/widgets.dart' hide Flow;
|
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
|
/// Displays a [LicensePage], which shows licenses for software used by the
|
||||||
/// application.
|
/// application.
|
||||||
///
|
///
|
||||||
@ -282,6 +339,28 @@ class AboutDialog extends StatelessWidget {
|
|||||||
this.children,
|
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.
|
/// The name of the application.
|
||||||
///
|
///
|
||||||
/// Defaults to the value of [Title.title], if a [Title] widget can be found.
|
/// 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.
|
/// A page that shows licenses for software used by the application.
|
||||||
///
|
///
|
||||||
/// To show a [LicensePage], use [showLicensePage].
|
/// To show a [LicensePage], use [showLicensePage].
|
||||||
|
@ -1841,6 +1841,141 @@ void main() {
|
|||||||
expect(renderParagraph.text.style!.color, theme.primaryTextTheme.titleLarge!.color);
|
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 {
|
class FakeLicenseEntry extends LicenseEntry {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user