Skip to content

Commit c8a3dba

Browse files
Add adaptive constructor to Radio and RadioListTile (#123816)
Add adaptive constructor to Radio and RadioListTile
1 parent d6593de commit c8a3dba

File tree

4 files changed

+208
-17
lines changed

4 files changed

+208
-17
lines changed

packages/flutter/lib/src/material/radio.dart

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5-
import 'package:flutter/widgets.dart';
5+
import 'package:flutter/cupertino.dart';
66

77
import 'color_scheme.dart';
88
import 'colors.dart';
@@ -20,6 +20,8 @@ import 'toggleable.dart';
2020
// late SingingCharacter? _character;
2121
// late StateSetter setState;
2222

23+
enum _RadioType { material, adaptive }
24+
2325
const double _kOuterRadius = 8.0;
2426
const double _kInnerRadius = 4.5;
2527

@@ -93,7 +95,40 @@ class Radio<T> extends StatefulWidget {
9395
this.visualDensity,
9496
this.focusNode,
9597
this.autofocus = false,
96-
});
98+
}) : _radioType = _RadioType.material;
99+
100+
/// Creates an adaptive [Radio] based on whether the target platform is iOS
101+
/// or macOS, following Material design's
102+
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
103+
///
104+
/// On iOS and macOS, this constructor creates a [CupertinoRadio], which has
105+
/// matching functionality and presentation as Material checkboxes, and are the
106+
/// graphics expected on iOS. On other platforms, this creates a Material
107+
/// design [Radio].
108+
///
109+
/// If a [CupertinoRadio] is created, the following parameters are ignored:
110+
/// [mouseCursor], [fillColor], [hoverColor], [overlayColor], [splashRadius],
111+
/// [materialTapTargetSize], [visualDensity].
112+
///
113+
/// The target platform is based on the current [Theme]: [ThemeData.platform].
114+
const Radio.adaptive({
115+
super.key,
116+
required this.value,
117+
required this.groupValue,
118+
required this.onChanged,
119+
this.mouseCursor,
120+
this.toggleable = false,
121+
this.activeColor,
122+
this.fillColor,
123+
this.focusColor,
124+
this.hoverColor,
125+
this.overlayColor,
126+
this.splashRadius,
127+
this.materialTapTargetSize,
128+
this.visualDensity,
129+
this.focusNode,
130+
this.autofocus = false,
131+
}) : _radioType = _RadioType.adaptive;
97132

98133
/// The value represented by this radio button.
99134
final T value;
@@ -309,6 +344,8 @@ class Radio<T> extends StatefulWidget {
309344
/// {@macro flutter.widgets.Focus.autofocus}
310345
final bool autofocus;
311346

347+
final _RadioType _radioType;
348+
312349
bool get _selected => value == groupValue;
313350

314351
@override
@@ -366,6 +403,33 @@ class _RadioState<T> extends State<Radio<T>> with TickerProviderStateMixin, Togg
366403
@override
367404
Widget build(BuildContext context) {
368405
assert(debugCheckHasMaterial(context));
406+
switch (widget._radioType) {
407+
case _RadioType.material:
408+
break;
409+
410+
case _RadioType.adaptive:
411+
final ThemeData theme = Theme.of(context);
412+
switch (theme.platform) {
413+
case TargetPlatform.android:
414+
case TargetPlatform.fuchsia:
415+
case TargetPlatform.linux:
416+
case TargetPlatform.windows:
417+
break;
418+
case TargetPlatform.iOS:
419+
case TargetPlatform.macOS:
420+
return CupertinoRadio<T>(
421+
value: widget.value,
422+
groupValue: widget.groupValue,
423+
onChanged: widget.onChanged,
424+
toggleable: widget.toggleable,
425+
activeColor: widget.activeColor,
426+
focusColor: widget.focusColor,
427+
focusNode: widget.focusNode,
428+
autofocus: widget.autofocus,
429+
);
430+
}
431+
}
432+
369433
final RadioThemeData radioTheme = RadioTheme.of(context);
370434
final RadioThemeData defaults = Theme.of(context).useMaterial3 ? _RadioDefaultsM3(context) : _RadioDefaultsM2(context);
371435
final MaterialTapTargetSize effectiveMaterialTapTargetSize = widget.materialTapTargetSize

packages/flutter/lib/src/material/radio_list_tile.dart

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import 'theme_data.dart';
1818
// enum SingingCharacter { lafayette }
1919
// late SingingCharacter? _character;
2020

21+
enum _RadioType { material, adaptive }
22+
2123
/// A [ListTile] with a [Radio]. In other words, a radio button with a label.
2224
///
2325
/// The entire list tile is interactive: tapping anywhere in the tile selects
@@ -186,7 +188,46 @@ class RadioListTile<T> extends StatelessWidget {
186188
this.focusNode,
187189
this.onFocusChange,
188190
this.enableFeedback,
189-
}) : assert(!isThreeLine || subtitle != null);
191+
}) : _radioType = _RadioType.material,
192+
assert(!isThreeLine || subtitle != null);
193+
194+
/// Creates a combination of a list tile and a platform adaptive radio.
195+
///
196+
/// The checkbox uses [Radio.adaptive] to show a [CupertinoRadio] for
197+
/// iOS platforms, or [Radio] for all others.
198+
///
199+
/// All other properties are the same as [RadioListTile].
200+
const RadioListTile.adaptive({
201+
super.key,
202+
required this.value,
203+
required this.groupValue,
204+
required this.onChanged,
205+
this.mouseCursor,
206+
this.toggleable = false,
207+
this.activeColor,
208+
this.fillColor,
209+
this.hoverColor,
210+
this.overlayColor,
211+
this.splashRadius,
212+
this.materialTapTargetSize,
213+
this.title,
214+
this.subtitle,
215+
this.isThreeLine = false,
216+
this.dense,
217+
this.secondary,
218+
this.selected = false,
219+
this.controlAffinity = ListTileControlAffinity.platform,
220+
this.autofocus = false,
221+
this.contentPadding,
222+
this.shape,
223+
this.tileColor,
224+
this.selectedTileColor,
225+
this.visualDensity,
226+
this.focusNode,
227+
this.onFocusChange,
228+
this.enableFeedback,
229+
}) : _radioType = _RadioType.adaptive,
230+
assert(!isThreeLine || subtitle != null);
190231

191232
/// The value represented by this radio button.
192233
final T value;
@@ -392,22 +433,44 @@ class RadioListTile<T> extends StatelessWidget {
392433
/// * [Feedback] for providing platform-specific feedback to certain actions.
393434
final bool? enableFeedback;
394435

436+
final _RadioType _radioType;
437+
395438
@override
396439
Widget build(BuildContext context) {
397-
final Widget control = Radio<T>(
398-
value: value,
399-
groupValue: groupValue,
400-
onChanged: onChanged,
401-
toggleable: toggleable,
402-
activeColor: activeColor,
403-
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
404-
autofocus: autofocus,
405-
fillColor: fillColor,
406-
mouseCursor: mouseCursor,
407-
hoverColor: hoverColor,
408-
overlayColor: overlayColor,
409-
splashRadius: splashRadius,
410-
);
440+
final Widget control;
441+
switch (_radioType) {
442+
case _RadioType.material:
443+
control = Radio<T>(
444+
value: value,
445+
groupValue: groupValue,
446+
onChanged: onChanged,
447+
toggleable: toggleable,
448+
activeColor: activeColor,
449+
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
450+
autofocus: autofocus,
451+
fillColor: fillColor,
452+
mouseCursor: mouseCursor,
453+
hoverColor: hoverColor,
454+
overlayColor: overlayColor,
455+
splashRadius: splashRadius,
456+
);
457+
case _RadioType.adaptive:
458+
control = Radio<T>.adaptive(
459+
value: value,
460+
groupValue: groupValue,
461+
onChanged: onChanged,
462+
toggleable: toggleable,
463+
activeColor: activeColor,
464+
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
465+
autofocus: autofocus,
466+
fillColor: fillColor,
467+
mouseCursor: mouseCursor,
468+
hoverColor: hoverColor,
469+
overlayColor: overlayColor,
470+
splashRadius: splashRadius,
471+
);
472+
}
473+
411474
Widget? leading, trailing;
412475
switch (controlAffinity) {
413476
case ListTileControlAffinity.leading:

packages/flutter/test/material/radio_list_tile_test.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Use of this source code is governed by a BSD-style license that can be
33
// found in the LICENSE file.
44

5+
import 'package:flutter/cupertino.dart';
56
import 'package:flutter/gestures.dart';
67
import 'package:flutter/material.dart';
78
import 'package:flutter/rendering.dart';
@@ -1241,6 +1242,37 @@ void main() {
12411242
expect(tester.getSize(find.byType(Radio<bool>)), const Size(48.0, 48.0));
12421243
});
12431244

1245+
testWidgets('RadioListTile.adaptive shows the correct radio platform widget', (WidgetTester tester) async {
1246+
Widget buildApp(TargetPlatform platform) {
1247+
return MaterialApp(
1248+
theme: ThemeData(platform: platform),
1249+
home: Material(
1250+
child: Center(
1251+
child: RadioListTile<int>.adaptive(
1252+
value: 1,
1253+
groupValue: 2,
1254+
onChanged: (_) {},
1255+
),
1256+
),
1257+
),
1258+
);
1259+
}
1260+
1261+
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
1262+
await tester.pumpWidget(buildApp(platform));
1263+
await tester.pumpAndSettle();
1264+
1265+
expect(find.byType(CupertinoRadio<int>), findsOneWidget);
1266+
}
1267+
1268+
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
1269+
await tester.pumpWidget(buildApp(platform));
1270+
await tester.pumpAndSettle();
1271+
1272+
expect(find.byType(CupertinoRadio<int>), findsNothing);
1273+
}
1274+
});
1275+
12441276
group('feedback', () {
12451277
late FeedbackTester feedback;
12461278

packages/flutter/test/material/radio_test.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ library;
99

1010
import 'dart:ui';
1111

12+
import 'package:flutter/cupertino.dart';
1213
import 'package:flutter/foundation.dart';
1314
import 'package:flutter/material.dart';
1415
import 'package:flutter/rendering.dart';
@@ -1372,4 +1373,35 @@ void main() {
13721373
: (paints..circle(color: theme.hoverColor)..circle(color: colors.secondary))
13731374
);
13741375
});
1376+
1377+
testWidgets('Radio.adaptive shows the correct platform widget', (WidgetTester tester) async {
1378+
Widget buildApp(TargetPlatform platform) {
1379+
return MaterialApp(
1380+
theme: ThemeData(platform: platform),
1381+
home: Material(
1382+
child: Center(
1383+
child: Radio<int>.adaptive(
1384+
value: 1,
1385+
groupValue: 2,
1386+
onChanged: (_) {},
1387+
),
1388+
),
1389+
),
1390+
);
1391+
}
1392+
1393+
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
1394+
await tester.pumpWidget(buildApp(platform));
1395+
await tester.pumpAndSettle();
1396+
1397+
expect(find.byType(CupertinoRadio<int>), findsOneWidget);
1398+
}
1399+
1400+
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
1401+
await tester.pumpWidget(buildApp(platform));
1402+
await tester.pumpAndSettle();
1403+
1404+
expect(find.byType(CupertinoRadio<int>), findsNothing);
1405+
}
1406+
});
13751407
}

0 commit comments

Comments
 (0)