Skip to content

Commit f4caee6

Browse files
Add adaptive Checkbox and CheckboxListTile (#123132)
Add adaptive Checkbox and CheckboxListTile
1 parent 785ea2a commit f4caee6

File tree

4 files changed

+231
-20
lines changed

4 files changed

+231
-20
lines changed

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

Lines changed: 75 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 'checkbox_theme.dart';
88
import 'color_scheme.dart';
@@ -18,6 +18,8 @@ import 'toggleable.dart';
1818
// bool _throwShotAway = false;
1919
// late StateSetter setState;
2020

21+
enum _CheckboxType { material, adaptive }
22+
2123
/// A Material Design checkbox.
2224
///
2325
/// The checkbox itself does not maintain any state. Instead, when the state of
@@ -88,7 +90,47 @@ class Checkbox extends StatefulWidget {
8890
this.shape,
8991
this.side,
9092
this.isError = false,
91-
}) : assert(tristate || value != null);
93+
}) : _checkboxType = _CheckboxType.material,
94+
assert(tristate || value != null);
95+
96+
/// Creates an adaptive [Checkbox] based on whether the target platform is iOS
97+
/// or macOS, following Material design's
98+
/// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html).
99+
///
100+
/// On iOS and macOS, this constructor creates a [CupertinoCheckbox], which has
101+
/// matching functionality and presentation as Material checkboxes, and are the
102+
/// graphics expected on iOS. On other platforms, this creates a Material
103+
/// design [Checkbox].
104+
///
105+
/// If a [CupertinoCheckbox] is created, the following parameters are ignored:
106+
/// [mouseCursor], [hoverColor], [overlayColor], [splashRadius],
107+
/// [materialTapTargetSize], [visualDensity], [isError]. However, [shape] and
108+
/// [side] will still affect the [CupertinoCheckbox] and should be handled if
109+
/// native fidelity is important.
110+
///
111+
/// The target platform is based on the current [Theme]: [ThemeData.platform].
112+
const Checkbox.adaptive({
113+
super.key,
114+
required this.value,
115+
this.tristate = false,
116+
required this.onChanged,
117+
this.mouseCursor,
118+
this.activeColor,
119+
this.fillColor,
120+
this.checkColor,
121+
this.focusColor,
122+
this.hoverColor,
123+
this.overlayColor,
124+
this.splashRadius,
125+
this.materialTapTargetSize,
126+
this.visualDensity,
127+
this.focusNode,
128+
this.autofocus = false,
129+
this.shape,
130+
this.side,
131+
this.isError = false,
132+
}) : _checkboxType = _CheckboxType.adaptive,
133+
assert(tristate || value != null);
92134

93135
/// Whether this checkbox is checked.
94136
///
@@ -347,6 +389,8 @@ class Checkbox extends StatefulWidget {
347389
/// The width of a checkbox widget.
348390
static const double width = 18.0;
349391

392+
final _CheckboxType _checkboxType;
393+
350394
@override
351395
State<Checkbox> createState() => _CheckboxState();
352396
}
@@ -410,6 +454,35 @@ class _CheckboxState extends State<Checkbox> with TickerProviderStateMixin, Togg
410454

411455
@override
412456
Widget build(BuildContext context) {
457+
switch (widget._checkboxType) {
458+
case _CheckboxType.material:
459+
break;
460+
461+
case _CheckboxType.adaptive:
462+
final ThemeData theme = Theme.of(context);
463+
switch (theme.platform) {
464+
case TargetPlatform.android:
465+
case TargetPlatform.fuchsia:
466+
case TargetPlatform.linux:
467+
case TargetPlatform.windows:
468+
break;
469+
case TargetPlatform.iOS:
470+
case TargetPlatform.macOS:
471+
return CupertinoCheckbox(
472+
value: value,
473+
tristate: tristate,
474+
onChanged: onChanged,
475+
activeColor: widget.activeColor,
476+
checkColor: widget.checkColor,
477+
focusColor: widget.focusColor,
478+
focusNode: widget.focusNode,
479+
autofocus: widget.autofocus,
480+
side: widget.side,
481+
shape: widget.shape,
482+
);
483+
}
484+
}
485+
413486
assert(debugCheckHasMaterial(context));
414487
final CheckboxThemeData checkboxTheme = CheckboxTheme.of(context);
415488
final CheckboxThemeData defaults = Theme.of(context).useMaterial3

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

Lines changed: 90 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import 'theme_data.dart';
1616
// late bool? _throwShotAway;
1717
// void setState(VoidCallback fn) { }
1818

19+
enum _CheckboxType { material, adaptive }
20+
1921
/// A [ListTile] with a [Checkbox]. In other words, a checkbox with a label.
2022
///
2123
/// The entire list tile is interactive: tapping anywhere in the tile toggles
@@ -192,7 +194,51 @@ class CheckboxListTile extends StatelessWidget {
192194
this.selectedTileColor,
193195
this.onFocusChange,
194196
this.enableFeedback,
195-
}) : assert(tristate || value != null),
197+
}) : _checkboxType = _CheckboxType.material,
198+
assert(tristate || value != null),
199+
assert(!isThreeLine || subtitle != null);
200+
201+
/// Creates a combination of a list tile and a platform adaptive checkbox.
202+
///
203+
/// The checkbox uses [Checkbox.adaptive] to show a [CupertinoCheckbox] for
204+
/// iOS platforms, or [Checkbox] for all others.
205+
///
206+
/// All other properties are the same as [CheckboxListTile].
207+
const CheckboxListTile.adaptive({
208+
super.key,
209+
required this.value,
210+
required this.onChanged,
211+
this.mouseCursor,
212+
this.activeColor,
213+
this.fillColor,
214+
this.checkColor,
215+
this.hoverColor,
216+
this.overlayColor,
217+
this.splashRadius,
218+
this.materialTapTargetSize,
219+
this.visualDensity,
220+
this.focusNode,
221+
this.autofocus = false,
222+
this.shape,
223+
this.side,
224+
this.isError = false,
225+
this.enabled,
226+
this.tileColor,
227+
this.title,
228+
this.subtitle,
229+
this.isThreeLine = false,
230+
this.dense,
231+
this.secondary,
232+
this.selected = false,
233+
this.controlAffinity = ListTileControlAffinity.platform,
234+
this.contentPadding,
235+
this.tristate = false,
236+
this.checkboxShape,
237+
this.selectedTileColor,
238+
this.onFocusChange,
239+
this.enableFeedback,
240+
}) : _checkboxType = _CheckboxType.adaptive,
241+
assert(tristate || value != null),
196242
assert(!isThreeLine || subtitle != null);
197243

198244
/// Whether this checkbox is checked.
@@ -406,6 +452,8 @@ class CheckboxListTile extends StatelessWidget {
406452
/// inoperative.
407453
final bool? enabled;
408454

455+
final _CheckboxType _checkboxType;
456+
409457
void _handleValueChange() {
410458
assert(onChanged != null);
411459
switch (value) {
@@ -420,23 +468,47 @@ class CheckboxListTile extends StatelessWidget {
420468

421469
@override
422470
Widget build(BuildContext context) {
423-
final Widget control = Checkbox(
424-
value: value,
425-
onChanged: enabled ?? true ? onChanged : null,
426-
mouseCursor: mouseCursor,
427-
activeColor: activeColor,
428-
fillColor: fillColor,
429-
checkColor: checkColor,
430-
hoverColor: hoverColor,
431-
overlayColor: overlayColor,
432-
splashRadius: splashRadius,
433-
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
434-
autofocus: autofocus,
435-
tristate: tristate,
436-
shape: checkboxShape,
437-
side: side,
438-
isError: isError,
439-
);
471+
final Widget control;
472+
473+
switch (_checkboxType) {
474+
case _CheckboxType.material:
475+
control = Checkbox(
476+
value: value,
477+
onChanged: enabled ?? true ? onChanged : null,
478+
mouseCursor: mouseCursor,
479+
activeColor: activeColor,
480+
fillColor: fillColor,
481+
checkColor: checkColor,
482+
hoverColor: hoverColor,
483+
overlayColor: overlayColor,
484+
splashRadius: splashRadius,
485+
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
486+
autofocus: autofocus,
487+
tristate: tristate,
488+
shape: checkboxShape,
489+
side: side,
490+
isError: isError,
491+
);
492+
case _CheckboxType.adaptive:
493+
control = Checkbox.adaptive(
494+
value: value,
495+
onChanged: enabled ?? true ? onChanged : null,
496+
mouseCursor: mouseCursor,
497+
activeColor: activeColor,
498+
fillColor: fillColor,
499+
checkColor: checkColor,
500+
hoverColor: hoverColor,
501+
overlayColor: overlayColor,
502+
splashRadius: splashRadius,
503+
materialTapTargetSize: materialTapTargetSize ?? MaterialTapTargetSize.shrinkWrap,
504+
autofocus: autofocus,
505+
tristate: tristate,
506+
shape: checkboxShape,
507+
side: side,
508+
isError: isError,
509+
);
510+
}
511+
440512
Widget? leading, trailing;
441513
switch (controlAffinity) {
442514
case ListTileControlAffinity.leading:

packages/flutter/test/material/checkbox_list_tile_test.dart

Lines changed: 33 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';
@@ -921,6 +922,38 @@ void main() {
921922
);
922923
});
923924

925+
testWidgets('CheckboxListTile.adaptive shows the correct checkbox platform widget', (WidgetTester tester) async {
926+
Widget buildApp(TargetPlatform platform) {
927+
return MaterialApp(
928+
theme: ThemeData(platform: platform),
929+
home: Material(
930+
child: Center(
931+
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
932+
return CheckboxListTile.adaptive(
933+
value: false,
934+
onChanged: (bool? newValue) {},
935+
);
936+
}),
937+
),
938+
),
939+
);
940+
}
941+
942+
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
943+
await tester.pumpWidget(buildApp(platform));
944+
await tester.pumpAndSettle();
945+
946+
expect(find.byType(CupertinoCheckbox), findsOneWidget);
947+
}
948+
949+
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
950+
await tester.pumpWidget(buildApp(platform));
951+
await tester.pumpAndSettle();
952+
953+
expect(find.byType(CupertinoCheckbox), findsNothing);
954+
}
955+
});
956+
924957
group('feedback', () {
925958
late FeedbackTester feedback;
926959

packages/flutter/test/material/checkbox_test.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import 'dart:ui';
66

7+
import 'package:flutter/cupertino.dart';
78
import 'package:flutter/foundation.dart';
89
import 'package:flutter/material.dart';
910
import 'package:flutter/rendering.dart';
@@ -1769,6 +1770,38 @@ void main() {
17691770
),
17701771
);
17711772
});
1773+
1774+
testWidgets('Checkbox.adaptive shows the correct platform widget', (WidgetTester tester) async {
1775+
Widget buildApp(TargetPlatform platform) {
1776+
return MaterialApp(
1777+
theme: ThemeData(platform: platform),
1778+
home: Material(
1779+
child: Center(
1780+
child: StatefulBuilder(builder: (BuildContext context, StateSetter setState) {
1781+
return Checkbox.adaptive(
1782+
value: false,
1783+
onChanged: (bool? newValue) {},
1784+
);
1785+
}),
1786+
),
1787+
),
1788+
);
1789+
}
1790+
1791+
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.iOS, TargetPlatform.macOS ]) {
1792+
await tester.pumpWidget(buildApp(platform));
1793+
await tester.pumpAndSettle();
1794+
1795+
expect(find.byType(CupertinoCheckbox), findsOneWidget);
1796+
}
1797+
1798+
for (final TargetPlatform platform in <TargetPlatform>[ TargetPlatform.android, TargetPlatform.fuchsia, TargetPlatform.linux, TargetPlatform.windows ]) {
1799+
await tester.pumpWidget(buildApp(platform));
1800+
await tester.pumpAndSettle();
1801+
1802+
expect(find.byType(CupertinoCheckbox), findsNothing);
1803+
}
1804+
});
17721805
}
17731806

17741807
class _SelectedGrabMouseCursor extends MaterialStateMouseCursor {

0 commit comments

Comments
 (0)