Add adaptive constructor to Radio and RadioListTile (#123816)

Add adaptive constructor to Radio and RadioListTile
diff --git a/packages/flutter/lib/src/material/radio.dart b/packages/flutter/lib/src/material/radio.dart
index 05fbbb6..7a7622e 100644
--- a/packages/flutter/lib/src/material/radio.dart
+++ b/packages/flutter/lib/src/material/radio.dart
@@ -2,7 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
-import 'package:flutter/widgets.dart';
+import 'package:flutter/cupertino.dart';
 
 import 'color_scheme.dart';
 import 'colors.dart';
@@ -20,6 +20,8 @@
 // late SingingCharacter? _character;
 // late StateSetter setState;
 
+enum _RadioType { material, adaptive }
+
 const double _kOuterRadius = 8.0;
 const double _kInnerRadius = 4.5;
 
@@ -93,7 +95,40 @@
     this.visualDensity,
     this.focusNode,
     this.autofocus = false,
-  });
+  }) : _radioType = _RadioType.material;
+
+  /// Creates an adaptive [Radio] 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).
+  ///
+  /// On iOS and macOS, this constructor creates a [CupertinoRadio], which has
+  /// matching functionality and presentation as Material checkboxes, and are the
+  /// graphics expected on iOS. On other platforms, this creates a Material
+  /// design [Radio].
+  ///
+  /// If a [CupertinoRadio] is created, the following parameters are ignored:
+  /// [mouseCursor], [fillColor], [hoverColor], [overlayColor], [splashRadius],
+  /// [materialTapTargetSize], [visualDensity].
+  ///
+  /// The target platform is based on the current [Theme]: [ThemeData.platform].
+  const Radio.adaptive({
+    super.key,
+    required this.value,
+    required this.groupValue,
+    required this.onChanged,
+    this.mouseCursor,
+    this.toggleable = false,
+    this.activeColor,
+    this.fillColor,
+    this.focusColor,
+    this.hoverColor,
+    this.overlayColor,
+    this.splashRadius,
+    this.materialTapTargetSize,
+    this.visualDensity,
+    this.focusNode,
+    this.autofocus = false,
+  }) : _radioType = _RadioType.adaptive;
 
   /// The value represented by this radio button.
   final T value;
@@ -309,6 +344,8 @@
   /// {@macro flutter.widgets.Focus.autofocus}
   final bool autofocus;
 
+  final _RadioType _radioType;
+
   bool get _selected => value == groupValue;
 
   @override
@@ -366,6 +403,33 @@
   @override
   Widget build(BuildContext context) {
     assert(debugCheckHasMaterial(context));
+    switch (widget._radioType) {
+      case _RadioType.material:
+        break;
+
+      case _RadioType.adaptive:
+        final ThemeData theme = Theme.of(context);
+        switch (theme.platform) {
+          case TargetPlatform.android:
+          case TargetPlatform.fuchsia:
+          case TargetPlatform.linux:
+          case TargetPlatform.windows:
+            break;
+          case TargetPlatform.iOS:
+          case TargetPlatform.macOS:
+            return CupertinoRadio<T>(
+              value: widget.value,
+              groupValue: widget.groupValue,
+              onChanged: widget.onChanged,
+              toggleable: widget.toggleable,
+              activeColor: widget.activeColor,
+              focusColor: widget.focusColor,
+              focusNode: widget.focusNode,
+              autofocus: widget.autofocus,
+            );
+        }
+    }
+
     final RadioThemeData radioTheme = RadioTheme.of(context);
     final RadioThemeData defaults = Theme.of(context).useMaterial3 ? _RadioDefaultsM3(context) : _RadioDefaultsM2(context);
     final MaterialTapTargetSize effectiveMaterialTapTargetSize = widget.materialTapTargetSize
diff --git a/packages/flutter/lib/src/material/radio_list_tile.dart b/packages/flutter/lib/src/material/radio_list_tile.dart
index 292748a..62a80c4 100644
--- a/packages/flutter/lib/src/material/radio_list_tile.dart
+++ b/packages/flutter/lib/src/material/radio_list_tile.dart
@@ -18,6 +18,8 @@
 // enum SingingCharacter { lafayette }
 // late SingingCharacter? _character;
 
+enum _RadioType { material, adaptive }
+
 /// A [ListTile] with a [Radio]. In other words, a radio button with a label.
 ///
 /// The entire list tile is interactive: tapping anywhere in the tile selects
@@ -186,7 +188,46 @@
     this.focusNode,
     this.onFocusChange,
     this.enableFeedback,
-  }) : assert(!isThreeLine || subtitle != null);
+  }) : _radioType = _RadioType.material,
+       assert(!isThreeLine || subtitle != null);
+
+  /// Creates a combination of a list tile and a platform adaptive radio.
+  ///
+  /// The checkbox uses [Radio.adaptive] to show a [CupertinoRadio] for
+  /// iOS platforms, or [Radio] for all others.
+  ///
+  /// All other properties are the same as [RadioListTile].
+  const RadioListTile.adaptive({
+    super.key,
+    required this.value,
+    required this.groupValue,
+    required this.onChanged,
+    this.mouseCursor,
+    this.toggleable = false,
+    this.activeColor,
+    this.fillColor,
+    this.hoverColor,
+    this.overlayColor,
+    this.splashRadius,
+    this.materialTapTargetSize,
+    this.title,
+    this.subtitle,
+    this.isThreeLine = false,
+    this.dense,
+    this.secondary,
+    this.selected = false,
+    this.controlAffinity = ListTileControlAffinity.platform,
+    this.autofocus = false,
+    this.contentPadding,
+    this.shape,
+    this.tileColor,
+    this.selectedTileColor,
+    this.visualDensity,
+    this.focusNode,
+    this.onFocusChange,
+    this.enableFeedback,
+  }) : _radioType = _RadioType.adaptive,
+       assert(!isThreeLine || subtitle != null);
 
   /// The value represented by this radio button.
   final T value;
@@ -392,22 +433,44 @@
   ///  * [Feedback] for providing platform-specific feedback to certain actions.
   final bool? enableFeedback;
 
+  final _RadioType _radioType;
+
   @override
   Widget build(BuildContext context) {
-    final Widget control = Radio<T>(
-      value: value,
-      groupValue: groupValue,
-      onChanged: onChanged,
-      toggleable: toggleable,
-      activeColor: activeColor,
-      materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
-      autofocus: autofocus,
-      fillColor: fillColor,
-      mouseCursor: mouseCursor,
-      hoverColor: hoverColor,
-      overlayColor: overlayColor,
-      splashRadius: splashRadius,
-    );
+    final Widget control;
+    switch (_radioType) {
+      case _RadioType.material:
+        control = Radio<T>(
+          value: value,
+          groupValue: groupValue,
+          onChanged: onChanged,
+          toggleable: toggleable,
+          activeColor: activeColor,
+          materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
+          autofocus: autofocus,
+          fillColor: fillColor,
+          mouseCursor: mouseCursor,
+          hoverColor: hoverColor,
+          overlayColor: overlayColor,
+          splashRadius: splashRadius,
+        );
+      case _RadioType.adaptive:
+        control = Radio<T>.adaptive(
+          value: value,
+          groupValue: groupValue,
+          onChanged: onChanged,
+          toggleable: toggleable,
+          activeColor: activeColor,
+          materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
+          autofocus: autofocus,
+          fillColor: fillColor,
+          mouseCursor: mouseCursor,
+          hoverColor: hoverColor,
+          overlayColor: overlayColor,
+          splashRadius: splashRadius,
+        );
+    }
+
     Widget? leading, trailing;
     switch (controlAffinity) {
       case ListTileControlAffinity.leading:
diff --git a/packages/flutter/test/material/radio_list_tile_test.dart b/packages/flutter/test/material/radio_list_tile_test.dart
index f947062..c852c9f 100644
--- a/packages/flutter/test/material/radio_list_tile_test.dart
+++ b/packages/flutter/test/material/radio_list_tile_test.dart
@@ -2,6 +2,7 @@
 // Use of this source code is governed by a BSD-style license that can be
 // found in the LICENSE file.
 
+import 'package:flutter/cupertino.dart';
 import 'package:flutter/gestures.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
@@ -1241,6 +1242,37 @@
     expect(tester.getSize(find.byType(Radio<bool>)), const Size(48.0, 48.0));
   });
 
+  testWidgets('RadioListTile.adaptive shows the correct radio platform widget', (WidgetTester tester) async {
+    Widget buildApp(TargetPlatform platform) {
+      return MaterialApp(
+        theme: ThemeData(platform: platform),
+        home: Material(
+          child: Center(
+            child: RadioListTile<int>.adaptive(
+              value: 1,
+              groupValue: 2,
+              onChanged: (_) {},
+            ),
+          ),
+        ),
+      );
+    }
+
+    for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
+      await tester.pumpWidget(buildApp(platform));
+      await tester.pumpAndSettle();
+
+      expect(find.byType(CupertinoRadio<int>), findsOneWidget);
+    }
+
+    for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
+      await tester.pumpWidget(buildApp(platform));
+      await tester.pumpAndSettle();
+
+      expect(find.byType(CupertinoRadio<int>), findsNothing);
+    }
+  });
+
   group('feedback', () {
     late FeedbackTester feedback;
 
diff --git a/packages/flutter/test/material/radio_test.dart b/packages/flutter/test/material/radio_test.dart
index 6e93da5..f10efde 100644
--- a/packages/flutter/test/material/radio_test.dart
+++ b/packages/flutter/test/material/radio_test.dart
@@ -9,6 +9,7 @@
 
 import 'dart:ui';
 
+import 'package:flutter/cupertino.dart';
 import 'package:flutter/foundation.dart';
 import 'package:flutter/material.dart';
 import 'package:flutter/rendering.dart';
@@ -1372,4 +1373,35 @@
         : (paints..circle(color: theme.hoverColor)..circle(color: colors.secondary))
     );
   });
+
+  testWidgets('Radio.adaptive shows the correct platform widget', (WidgetTester tester) async {
+    Widget buildApp(TargetPlatform platform) {
+      return MaterialApp(
+        theme: ThemeData(platform: platform),
+        home: Material(
+          child: Center(
+            child: Radio<int>.adaptive(
+              value: 1,
+              groupValue: 2,
+              onChanged: (_) {},
+            ),
+          ),
+        ),
+      );
+    }
+
+    for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
+      await tester.pumpWidget(buildApp(platform));
+      await tester.pumpAndSettle();
+
+      expect(find.byType(CupertinoRadio<int>), findsOneWidget);
+    }
+
+    for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
+      await tester.pumpWidget(buildApp(platform));
+      await tester.pumpAndSettle();
+
+      expect(find.byType(CupertinoRadio<int>), findsNothing);
+    }
+  });
 }