diff --git a/packages/flutter/lib/src/material/about.dart b/packages/flutter/lib/src/material/about.dart index 6cc016ca0f..251dd2dcff 100644 --- a/packages/flutter/lib/src/material/about.dart +++ b/packages/flutter/lib/src/material/about.dart @@ -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? children, + bool barrierDismissible = true, + Color? barrierColor, + String? barrierLabel, + bool useRootNavigator = true, + RouteSettings? routeSettings, + Offset? anchorPoint, +}) { + showAdaptiveDialog( + 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? 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? _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 [ + 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 [ + 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? actions = _actions(context); + + return AlertDialog.adaptive( + content: ListBody( + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (icon != null) IconTheme(data: themeData.iconTheme, child: icon), + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24.0), + child: ListBody( + children: [ + 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]. diff --git a/packages/flutter/test/material/about_test.dart b/packages/flutter/test/material/about_test.dart index afa52d2a25..20c82598e3 100644 --- a/packages/flutter/test/material/about_test.dart +++ b/packages/flutter/test/material/about_test.dart @@ -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.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: [ + const Text('Test1'), + ], + ); + + await tester.pumpAndSettle(const Duration(seconds: 1)); + expect(find.byType(CupertinoDialogAction), findsWidgets); + } + + for (final TargetPlatform platform in [ + 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.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.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 {