Introduced the `RadioGroup` widget to centralize `groupValue` management and the `onChanged` callback for a set of `Radio` widgets. As a result, the individual `Radio.groupValue` and `Radio.onChanged` properties have been deprecated.
Original behavior
To meet APG (ARIA Practices Guide) requirements for keyboard navigation and semantic properties in radio button groups, Flutter needed a dedicated radio group concept. Introducing a wrapper widget, `RadioGroup`, provides this out-of-the-box support. This change also presented an opportunity to simplify the API for individual `Radio` widgets.
New behavior
The following API is deprecated:
- Who is affected
- Applications and packages relying on the previous behaviour or signature. Code that uses RadioGroup, Radio, Radio.groupValue and related APIs.
- Practical impact
- Behaviour or API differences that can break builds or change runtime behaviour.
- What action should I take?
- Follow the official migration guide below, update affected call sites, run `dart fix --apply` where a fix is available, then run your tests.
Before and after (from the official guide)
| 1 | Widget build(BuildContext context) { | |
| 2 | − | return Column( |
| 3 | − | children: <Widget>[ |
| 4 | − | Radio<int>( |
| 5 | − | value: 0, |
| 6 | − | groupValue: _groupValue, |
| 7 | − | onChanged: (int? value) { |
| 8 | − | setState(() { |
| 9 | − | _groupValue = value; |
| 10 | − | }); |
| 11 | − | }, |
| 12 | − | ), |
| 13 | − | Radio<int>( |
| 14 | − | value: 2, |
| 15 | − | groupValue: _groupValue, |
| 16 | − | onChanged: (int? value) { |
| 17 | − | setState(() { |
| 18 | − | _groupValue = value; |
| 19 | − | }); |
| 20 | − | }, |
| 21 | − | ), |
| 22 | − | ], |
| 23 | ); | |
| 24 | } |
| 1 | Widget build(BuildContext context) { | |
| 2 | + | return RadioGroup<int>( |
| 3 | + | groupValue: _groupValue, |
| 4 | + | onChanged: (int? value) { |
| 5 | + | setState(() { |
| 6 | + | _groupValue = value; |
| 7 | + | }); |
| 8 | + | }, |
| 9 | + | child: Column( |
| 10 | + | children: <Widget>[ |
| 11 | + | Radio<int>(value: 0), |
| 12 | + | Radio<int>(value: 2), |
| 13 | + | ], |
| 14 | + | ), |
| 15 | ); | |
| 16 | } |
Raw before / after
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Radio<int>(
value: 0,
groupValue: _groupValue,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
Radio<int>(
value: 2,
groupValue: _groupValue,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
],
);
}Widget build(BuildContext context) {
return RadioGroup<int>(
groupValue: _groupValue,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
child: Column(
children: <Widget>[
Radio<int>(value: 0),
Radio<int>(value: 2),
],
),
);
}Official migration guide (excerpt, © The Flutter Authors, CC BY 3.0)
If you are using these properties, you can refactor them with RadioGroup.
Case 1: trivial case
Code before migration:
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Radio<int>(
value: 0,
groupValue: _groupValue,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
Radio<int>(
value: 2,
groupValue: _groupValue,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
],
);
}Code after migration:
Widget build(BuildContext context) {
return RadioGroup<int>(
groupValue: _groupValue,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
child: Column(
children: <Widget>[
Radio<int>(value: 0),
Radio<int>(value: 2),
],
),
);
}Case 2: disabled radio
Code before migration:
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Radio<int>(
value: 0,
groupValue: _groupValue,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
Radio<int>(
value: 2,
groupValue: _groupValue,
onChanged: null, // disabled
),
],
);
}Code after migration:
Widget build(BuildContext context) {
return RadioGroup<int>(
groupValue: _groupValue,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
child: Column(
children: <Widget>[
Radio<int>(value: 0),
Radio<int>(value: 2, enabled: false),
],
),
);
}Case 3: mixed group or multi-selection
Code before migration:
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Radio<int>(
value: 1,
groupValue: _groupValue,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
}, // disabled
),
Radio<String>(
value: 'a',
groupValue: _stringValue,
onChanged: (String? value) {
setState(() {
_stringValue = value;
});
},
),
Radio<String>(
value: 'b',
groupValue: _stringValue,
onChanged: (String? value) {
setState(() {
_stringValue = value;
});
},
),
Radio<int>(
value: 2,
groupValue: _groupValue,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
}, // disabled
),
],
);
}Code after migration:
Widget build(BuildContext context) {
return RadioGroup<int>(
groupValue: _groupValue,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
child: Column(
children: <Widget>[
Radio<int>(value: 1),
RadioGroup<String>(
child: Column(
children: <Widget>[
Radio<String>(value: 'a'),
Radio<String>(value: 'b'),
]
),
),
Radio<int>(value: 2),
],
),
);
}AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.Listed in the official breaking-changes index (docs.flutter.dev/release/breaking-changes).
An official announcement or migration guide documents an intended behaviour or API change. A verified source change (git diff) shows code changed but does not by itself prove a public API is breaking. AI interpretations are labelled and never used to classify a change.
- Source commit
- b3045439b7c0
- Source last updated
- Oct 21, 2026
- Last synchronized
- Oct 21, 2026
- Publication status
- published
Publication history
- Oct 21, 2026, 8:31 PM: new → published (ingestion) — Backed by an official breaking-change/migration guide.
AI_PROVIDER (for example to openai-compatible with a local Ollama or LM Studio endpoint) to enable grounded explanations. Everything else on this page works without an AI provider.