Adaptive icons (#69119)

* refactor update_icons

* fix trailing space

* address feedback

* add platform adaptive icons

* fix merge conflict

* Update dartdoc

* Address feedback

* Specify types

* Add tests

* fix indentation

* Remove trailing space

* Remove isCupertino static bool
diff --git a/dev/tools/update_icons.dart b/dev/tools/update_icons.dart
index 41a5c4c..cc0bed7 100644
--- a/dev/tools/update_icons.dart
+++ b/dev/tools/update_icons.dart
@@ -20,8 +20,20 @@
 const String _defaultOldCodepointsPath = 'bin/cache/artifacts/material_fonts/codepoints';
 const String _defaultIconsPath = 'packages/flutter/lib/src/material/icons.dart';
 
-const String _beginGeneratedMark = '// BEGIN GENERATED';
-const String _endGeneratedMark = '// END GENERATED';
+const String _beginGeneratedMark = '// BEGIN GENERATED ICONS';
+const String _endGeneratedMark = '// END GENERATED ICONS';
+const String _beginPlatformAdaptiveGeneratedMark = '// BEGIN GENERATED PLATFORM ADAPTIVE ICONS';
+const String _endPlatformAdaptiveGeneratedMark = '// END GENERATED PLATFORM ADAPTIVE ICONS';
+
+const Map<String, List<String>> _platformAdaptiveIdentifiers = <String, List<String>>{
+  // Mapping of Flutter IDs to an Android/agnostic ID and an iOS ID.
+  // Flutter IDs can be anything, but should be chosen to be agnostic.
+  'arrow_back': <String>['arrow_back', 'arrow_back_ios'],
+  'arrow_forward': <String>['arrow_forward', 'arrow_forward_ios'],
+  'flip_camera': <String>['flip_camera_android', 'flip_camera_ios'],
+  'more': <String>['more_vert', 'more_horiz'],
+  'share': <String>['share', 'ios_share'],
+};
 
 const Map<String, String> _identifierRewrites = <String, String>{
   '360': 'threesixty',
@@ -231,20 +243,54 @@
 
 // Do not make this method private as it is used by g3 roll.
 String regenerateIconsFile(String iconData, Map<String, String> tokenPairMap) {
+  final Iterable<_Icon> newIcons = tokenPairMap.entries.map((MapEntry<String, String> entry) => _Icon(entry));
   final StringBuffer buf = StringBuffer();
   bool generating = false;
+
   for (final String line in LineSplitter.split(iconData)) {
     if (!generating) {
       buf.writeln(line);
     }
-    if (line.contains(_beginGeneratedMark)) {
+
+    // Generate for _PlatformAdaptiveIcons
+    if (line.contains(_beginPlatformAdaptiveGeneratedMark)) {
       generating = true;
 
-      final String iconDeclarationsString = <String>[
-        for (MapEntry<String, String> entry in tokenPairMap.entries)
-          _Icon(entry).fullDeclaration
-      ].join();
+      final List<String> platformAdaptiveDeclarations = <String>[];
+      _platformAdaptiveIdentifiers.forEach((String flutterId, List<String> ids) {
+        // Automatically finds and generates styled icon declarations.
+        for (final IconStyle iconStyle in IconStyle.values) {
+          final String style = iconStyle.idSuffix();
+          try {
+            final _Icon agnosticIcon = newIcons.firstWhere(
+                (_Icon icon) => icon.id == '${ids[0]}$style',
+                orElse: () => throw ids[0]);
+            final _Icon iOSIcon = newIcons.firstWhere(
+                (_Icon icon) => icon.id == '${ids[1]}$style',
+                orElse: () => throw ids[1]);
 
+            platformAdaptiveDeclarations.add(_Icon.platformAdaptiveDeclaration('$flutterId$style', agnosticIcon, iOSIcon));
+          } catch (e) {
+            if (iconStyle == IconStyle.regular) {
+              stderr.writeln("Error while generating platformAdaptiveDeclarations: Icon '$e' not found.");
+              exit(1);
+            } else {
+              // Ignore errors for styled icons since some don't exist.
+            }
+          }
+        }
+      });
+
+      buf.write(platformAdaptiveDeclarations.join());
+    } else if (line.contains(_endPlatformAdaptiveGeneratedMark)) {
+      generating = false;
+      buf.writeln(line);
+    }
+
+    // Generate for Icons
+    if (line.contains(_beginGeneratedMark)) {
+      generating = true;
+      final String iconDeclarationsString = newIcons.map((_Icon icon) => icon.fullDeclaration).join('');
       buf.write(iconDeclarationsString);
     } else if (line.contains(_endGeneratedMark)) {
       generating = false;
@@ -275,9 +321,9 @@
   sharp,
 }
 
-extension IconStyleSuffix on IconStyle {
+extension IconStyleExtension on IconStyle {
   // The suffix for the 'material-icons' HTML class.
-  String suffix() {
+  String htmlSuffix() {
     switch (this) {
       case IconStyle.outlined: return '-outlined';
       case IconStyle.rounded: return '-round';
@@ -285,6 +331,17 @@
       default: return '';
     }
   }
+
+  // The suffix for icon ids.
+  String idSuffix() {
+    switch (this) {
+      case IconStyle.outlined:
+      case IconStyle.rounded:
+      case IconStyle.sharp:
+        return '_' + toString().split('.').last;
+      default: return '';
+    }
+  }
 }
 
 class _Icon {
@@ -332,12 +389,25 @@
   String get name => id.replaceAll('_', ' ');
 
   String get dartDoc =>
-      '/// <i class="material-icons${style.suffix()} md-36">$shortId</i> &#x2014; material icon named "$name".';
+      '<i class="material-icons${style.htmlSuffix()} md-36">$shortId</i> &#x2014; material icon named "$name"';
 
   String get declaration =>
       "static const IconData $flutterId = IconData(0x$hexCodepoint, fontFamily: 'MaterialIcons'$mirroredInRTL);";
 
-  String get fullDeclaration => '''\n  $dartDoc\n  $declaration\n''';
+  String get fullDeclaration => '''
+
+  /// $dartDoc.
+  $declaration
+''';
+
+  static String platformAdaptiveDeclaration(String flutterId, _Icon agnosticIcon, _Icon iOSIcon) => '''
+
+  /// Platform-adaptive icon for ${agnosticIcon.dartDoc} and ${iOSIcon.dartDoc}.;
+  IconData get $flutterId => !_isCupertino() ? Icons.${agnosticIcon.flutterId} : Icons.${iOSIcon.flutterId};
+''';
+
+  @override
+  String toString() => id;
 }
 
 // Replace the old codepoints file with the new.
diff --git a/packages/flutter/lib/src/material/icons.dart b/packages/flutter/lib/src/material/icons.dart
index f39bc80..65d5880 100644
--- a/packages/flutter/lib/src/material/icons.dart
+++ b/packages/flutter/lib/src/material/icons.dart
@@ -2,8 +2,81 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter/foundation.dart' show defaultTargetPlatform;
 import 'package:flutter/widgets.dart';
 
+// ignore_for_file: non_constant_identifier_names
+class _PlatformAdaptiveIcons {
+  static bool _isCupertino() {
+    switch (defaultTargetPlatform) {
+      case TargetPlatform.android:
+      case TargetPlatform.fuchsia:
+      case TargetPlatform.linux:
+      case TargetPlatform.windows:
+        return false;
+      case TargetPlatform.iOS:
+      case TargetPlatform.macOS:
+        return true;
+    }
+  }
+
+  // Generated code: do not hand-edit.
+  // See https://github.com/flutter/flutter/wiki/Updating-Material-Design-Fonts
+  // BEGIN GENERATED PLATFORM ADAPTIVE ICONS
+
+  /// Platform-adaptive icon for <i class="material-icons md-36">arrow_back</i> &#x2014; material icon named "arrow back" and <i class="material-icons md-36">arrow_back_ios</i> &#x2014; material icon named "arrow back ios".;
+  IconData get arrow_back => !_isCupertino() ? Icons.arrow_back : Icons.arrow_back_ios;
+
+  /// Platform-adaptive icon for <i class="material-icons-outlined md-36">arrow_back</i> &#x2014; material icon named "arrow back outlined" and <i class="material-icons-outlined md-36">arrow_back_ios</i> &#x2014; material icon named "arrow back ios outlined".;
+  IconData get arrow_back_outlined => !_isCupertino() ? Icons.arrow_back_outlined : Icons.arrow_back_ios_outlined;
+
+  /// Platform-adaptive icon for <i class="material-icons-round md-36">arrow_back</i> &#x2014; material icon named "arrow back rounded" and <i class="material-icons-round md-36">arrow_back_ios</i> &#x2014; material icon named "arrow back ios rounded".;
+  IconData get arrow_back_rounded => !_isCupertino() ? Icons.arrow_back_rounded : Icons.arrow_back_ios_rounded;
+
+  /// Platform-adaptive icon for <i class="material-icons-sharp md-36">arrow_back</i> &#x2014; material icon named "arrow back sharp" and <i class="material-icons-sharp md-36">arrow_back_ios</i> &#x2014; material icon named "arrow back ios sharp".;
+  IconData get arrow_back_sharp => !_isCupertino() ? Icons.arrow_back_sharp : Icons.arrow_back_ios_sharp;
+
+  /// Platform-adaptive icon for <i class="material-icons md-36">arrow_forward</i> &#x2014; material icon named "arrow forward" and <i class="material-icons md-36">arrow_forward_ios</i> &#x2014; material icon named "arrow forward ios".;
+  IconData get arrow_forward => !_isCupertino() ? Icons.arrow_forward : Icons.arrow_forward_ios;
+
+  /// Platform-adaptive icon for <i class="material-icons-outlined md-36">arrow_forward</i> &#x2014; material icon named "arrow forward outlined" and <i class="material-icons-outlined md-36">arrow_forward_ios</i> &#x2014; material icon named "arrow forward ios outlined".;
+  IconData get arrow_forward_outlined => !_isCupertino() ? Icons.arrow_forward_outlined : Icons.arrow_forward_ios_outlined;
+
+  /// Platform-adaptive icon for <i class="material-icons-round md-36">arrow_forward</i> &#x2014; material icon named "arrow forward rounded" and <i class="material-icons-round md-36">arrow_forward_ios</i> &#x2014; material icon named "arrow forward ios rounded".;
+  IconData get arrow_forward_rounded => !_isCupertino() ? Icons.arrow_forward_rounded : Icons.arrow_forward_ios_rounded;
+
+  /// Platform-adaptive icon for <i class="material-icons-sharp md-36">arrow_forward</i> &#x2014; material icon named "arrow forward sharp" and <i class="material-icons-sharp md-36">arrow_forward_ios</i> &#x2014; material icon named "arrow forward ios sharp".;
+  IconData get arrow_forward_sharp => !_isCupertino() ? Icons.arrow_forward_sharp : Icons.arrow_forward_ios_sharp;
+
+  /// Platform-adaptive icon for <i class="material-icons md-36">flip_camera_android</i> &#x2014; material icon named "flip camera android" and <i class="material-icons md-36">flip_camera_ios</i> &#x2014; material icon named "flip camera ios".;
+  IconData get flip_camera => !_isCupertino() ? Icons.flip_camera_android : Icons.flip_camera_ios;
+
+  /// Platform-adaptive icon for <i class="material-icons-outlined md-36">flip_camera_android</i> &#x2014; material icon named "flip camera android outlined" and <i class="material-icons-outlined md-36">flip_camera_ios</i> &#x2014; material icon named "flip camera ios outlined".;
+  IconData get flip_camera_outlined => !_isCupertino() ? Icons.flip_camera_android_outlined : Icons.flip_camera_ios_outlined;
+
+  /// Platform-adaptive icon for <i class="material-icons-round md-36">flip_camera_android</i> &#x2014; material icon named "flip camera android rounded" and <i class="material-icons-round md-36">flip_camera_ios</i> &#x2014; material icon named "flip camera ios rounded".;
+  IconData get flip_camera_rounded => !_isCupertino() ? Icons.flip_camera_android_rounded : Icons.flip_camera_ios_rounded;
+
+  /// Platform-adaptive icon for <i class="material-icons-sharp md-36">flip_camera_android</i> &#x2014; material icon named "flip camera android sharp" and <i class="material-icons-sharp md-36">flip_camera_ios</i> &#x2014; material icon named "flip camera ios sharp".;
+  IconData get flip_camera_sharp => !_isCupertino() ? Icons.flip_camera_android_sharp : Icons.flip_camera_ios_sharp;
+
+  /// Platform-adaptive icon for <i class="material-icons md-36">more_vert</i> &#x2014; material icon named "more vert" and <i class="material-icons md-36">more_horiz</i> &#x2014; material icon named "more horiz".;
+  IconData get more => !_isCupertino() ? Icons.more_vert : Icons.more_horiz;
+
+  /// Platform-adaptive icon for <i class="material-icons-outlined md-36">more_vert</i> &#x2014; material icon named "more vert outlined" and <i class="material-icons-outlined md-36">more_horiz</i> &#x2014; material icon named "more horiz outlined".;
+  IconData get more_outlined => !_isCupertino() ? Icons.more_vert_outlined : Icons.more_horiz_outlined;
+
+  /// Platform-adaptive icon for <i class="material-icons-round md-36">more_vert</i> &#x2014; material icon named "more vert rounded" and <i class="material-icons-round md-36">more_horiz</i> &#x2014; material icon named "more horiz rounded".;
+  IconData get more_rounded => !_isCupertino() ? Icons.more_vert_rounded : Icons.more_horiz_rounded;
+
+  /// Platform-adaptive icon for <i class="material-icons-sharp md-36">more_vert</i> &#x2014; material icon named "more vert sharp" and <i class="material-icons-sharp md-36">more_horiz</i> &#x2014; material icon named "more horiz sharp".;
+  IconData get more_sharp => !_isCupertino() ? Icons.more_vert_sharp : Icons.more_horiz_sharp;
+
+  /// Platform-adaptive icon for <i class="material-icons md-36">share</i> &#x2014; material icon named "share" and <i class="material-icons md-36">ios_share</i> &#x2014; material icon named "ios share".;
+  IconData get share => !_isCupertino() ? Icons.share : Icons.ios_share;
+  // END GENERATED PLATFORM ADAPTIVE ICONS
+}
+
 /// Identifiers for the supported material design icons.
 ///
 /// Use with the [Icon] class to show specific icons.
@@ -64,9 +137,35 @@
   // ignore: unused_element
   Icons._();
 
+  /// A set of platform-adaptive material design icons.
+  ///
+  /// Provides a convenient way to show a certain set of platform-appropriate
+  /// icons on Apple platforms.
+  ///
+  /// Use with the [Icon] class to show specific icons.
+  ///
+  /// {@tool snippet}
+  /// This example shows how to create a share icon that uses the material icon
+  /// named "share" on non-Apple platforms, and the icon named "ios share" on
+  /// Apple platforms.
+  ///
+  /// ```dart
+  /// Icon(
+  ///   Icons.adaptive.share,
+  /// )
+  /// ```
+  /// {@end-tool}
+  ///
+  /// See also:
+  ///
+  ///  * [Icon]
+  ///  * [IconButton]
+  ///  * <https://design.google.com/icons/>
+  static _PlatformAdaptiveIcons get adaptive => _PlatformAdaptiveIcons();
+
   // Generated code: do not hand-edit.
   // See https://github.com/flutter/flutter/wiki/Updating-Material-Design-Fonts
-  // BEGIN GENERATED
+  // BEGIN GENERATED ICONS
 
   /// <i class="material-icons md-36">10k</i> &#x2014; material icon named "10k".
   static const IconData ten_k = IconData(0xe52a, fontFamily: 'MaterialIcons');
@@ -17029,5 +17128,5 @@
 
   /// <i class="material-icons-sharp md-36">zoom_out</i> &#x2014; material icon named "zoom out sharp".
   static const IconData zoom_out_sharp = IconData(0xf02d, fontFamily: 'MaterialIcons');
-  // END GENERATED
+  // END GENERATED ICONS
 }
diff --git a/packages/flutter/test/material/icons_test.dart b/packages/flutter/test/material/icons_test.dart
index 06a30a7..6ba3989 100644
--- a/packages/flutter/test/material/icons_test.dart
+++ b/packages/flutter/test/material/icons_test.dart
@@ -16,4 +16,29 @@
     expect(Icons.clear.fontFamily, 'MaterialIcons');
     expect(Icons.search.fontFamily, 'MaterialIcons');
   });
+
+  testWidgets('Adaptive icons are correct on cupertino platforms',
+        (WidgetTester tester) async {
+      expect(Icons.adaptive.arrow_back, Icons.arrow_back_ios);
+      expect(Icons.adaptive.arrow_back_outlined, Icons.arrow_back_ios_outlined);
+    },
+    variant: const TargetPlatformVariant(<TargetPlatform>{
+      TargetPlatform.iOS,
+      TargetPlatform.macOS,
+    }),
+  );
+
+  testWidgets(
+    'Adaptive icons are correct on non-cupertino platforms',
+        (WidgetTester tester) async {
+      expect(Icons.adaptive.arrow_back, Icons.arrow_back);
+      expect(Icons.adaptive.arrow_back_outlined, Icons.arrow_back_outlined);
+    },
+    variant: const TargetPlatformVariant(<TargetPlatform>{
+      TargetPlatform.android,
+      TargetPlatform.fuchsia,
+      TargetPlatform.windows,
+      TargetPlatform.linux,
+    }),
+  );
 }