Skip to content

Commit dc1f69b

Browse files
authored
Fix RadioGroup single selection check. (flutter#175654)
Fixes flutter#175258 ### Description This PR fixes the single selection check in `RadioGroup` by delaying it to the next frame, which ensures that all `registerClient` and `unregisterClient` calls are processed. The same pattern can be found in `_TabBarState._debugScheduleCheckHasValidTabsCount` and `RawScrollbarState._debugScheduleCheckHasValidScrollPosition`. ## Pre-launch Checklist - [X] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [X] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [X] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [X] I signed the [CLA]. - [X] I listed at least one issue that this PR fixes in the description above. - [ ] I updated/added relevant documentation (doc comments with `///`). - [X] I added new tests to check the change I am making, or this PR is [test-exempt]. - [X] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [X] All existing and new tests are passing. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
1 parent 7783b94 commit dc1f69b

2 files changed

Lines changed: 108 additions & 4 deletions

File tree

packages/flutter/lib/src/widgets/radio_group.dart

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:flutter/services.dart';
99

1010
import 'actions.dart';
1111
import 'basic.dart';
12+
import 'binding.dart';
1213
import 'focus_manager.dart';
1314
import 'focus_traversal.dart';
1415
import 'framework.dart';
@@ -96,6 +97,27 @@ class _RadioGroupState<T> extends State<RadioGroup<T>> implements RadioGroupRegi
9697

9798
final Set<RadioClient<T>> _radios = <RadioClient<T>>{};
9899

100+
bool _debugHasScheduledSingleSelectionCheck = false;
101+
102+
/// Schedules a check for the next frame to verify that there is only one
103+
/// radio with the group value.
104+
bool _debugScheduleSingleSelectionCheck() {
105+
if (_debugHasScheduledSingleSelectionCheck) {
106+
return true;
107+
}
108+
WidgetsBinding.instance.addPostFrameCallback((_) {
109+
_debugHasScheduledSingleSelectionCheck = false;
110+
if (!mounted || _debugCheckOnlySingleSelection()) {
111+
return;
112+
}
113+
throw FlutterError(
114+
"RadioGroupPolicy can't be used for a radio group that allows multiple selection.",
115+
);
116+
}, debugLabel: 'RadioGroup.singleSelectionCheck');
117+
_debugHasScheduledSingleSelectionCheck = true;
118+
return true;
119+
}
120+
99121
bool _debugCheckOnlySingleSelection() {
100122
return _radios.where((RadioClient<T> radio) => radio.radioValue == groupValue).length < 2;
101123
}
@@ -106,10 +128,7 @@ class _RadioGroupState<T> extends State<RadioGroup<T>> implements RadioGroupRegi
106128
@override
107129
void registerClient(RadioClient<T> radio) {
108130
_radios.add(radio);
109-
assert(
110-
_debugCheckOnlySingleSelection(),
111-
"RadioGroupPolicy can't be used for a radio group that allows multiple selection",
112-
);
131+
assert(_debugScheduleSingleSelectionCheck());
113132
}
114133

115134
@override
@@ -176,6 +195,8 @@ class _RadioGroupState<T> extends State<RadioGroup<T>> implements RadioGroupRegi
176195

177196
@override
178197
Widget build(BuildContext context) {
198+
assert(_debugScheduleSingleSelectionCheck());
199+
179200
return Semantics(
180201
container: true,
181202
role: SemanticsRole.radioGroup,

packages/flutter/test/widgets/radio_group_test.dart

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -244,6 +244,89 @@ void main() {
244244
expect(radio1.hasFocus, isTrue);
245245
expect(textFieldAfter.hasFocus, isFalse);
246246
});
247+
248+
// Regression test for https://github.com/flutter/flutter/issues/175258.
249+
testWidgets('Radio group throws on multiple selection', (WidgetTester tester) async {
250+
final UniqueKey key1 = UniqueKey();
251+
await tester.pumpWidget(
252+
MaterialApp(
253+
home: Material(
254+
child: TestRadioGroup<int>(
255+
child: Column(
256+
children: <Widget>[
257+
const Radio<int>(value: 0),
258+
Radio<int>(key: key1, value: 1),
259+
const Radio<int>(value: 1),
260+
const Radio<int>(value: 2),
261+
],
262+
),
263+
),
264+
),
265+
),
266+
);
267+
268+
expect(tester.takeException(), isNull);
269+
270+
await tester.tap(find.byKey(key1));
271+
await tester.pump();
272+
273+
expect(
274+
tester.takeException(),
275+
isA<FlutterError>().having(
276+
(FlutterError e) => e.message,
277+
'message',
278+
"RadioGroupPolicy can't be used for a radio group that allows multiple selection.",
279+
),
280+
);
281+
});
282+
283+
// Regression test for https://github.com/flutter/flutter/issues/175258.
284+
testWidgets('Radio group does not throw when number of children decreases', (
285+
WidgetTester tester,
286+
) async {
287+
await tester.pumpWidget(
288+
MaterialApp(
289+
home: Material(
290+
child: RadioGroup<int>(
291+
onChanged: (_) {},
292+
groupValue: 4,
293+
child: const Column(
294+
children: <Widget>[
295+
Radio<int>(value: 0),
296+
Radio<int>(value: 1),
297+
Radio<int>(value: 2),
298+
Radio<int>(value: 3),
299+
Radio<int>(value: 4),
300+
],
301+
),
302+
),
303+
),
304+
),
305+
);
306+
307+
expect(tester.takeException(), isNull);
308+
309+
await tester.pumpWidget(
310+
MaterialApp(
311+
home: Material(
312+
child: RadioGroup<int>(
313+
onChanged: (_) {},
314+
groupValue: 4,
315+
child: const Column(
316+
children: <Widget>[
317+
Radio<int>(value: 1),
318+
Radio<int>(value: 2),
319+
Radio<int>(value: 3),
320+
Radio<int>(value: 4),
321+
],
322+
),
323+
),
324+
),
325+
),
326+
);
327+
328+
expect(tester.takeException(), isNull);
329+
});
247330
}
248331

249332
class TestRadioGroup<T> extends StatefulWidget {

0 commit comments

Comments
 (0)