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);
+ }
+ });
}