How to Implement a Flutter Radio Button Group for User Selections
Use RadioGroup<T> as an inherited container to manage the groupValue and onChanged callback for all child radio widgets, eliminating the need to pass state to individual Radio or RadioListTile instances.
Flutter provides a modern, centralized approach to handling mutually exclusive selections through the RadioGroup<T> widget. According to the flutter/flutter repository, this widget acts as a state container that automatically manages keyboard navigation, accessibility semantics, and single-selection enforcement for any radio-style widgets nested within it.
Understanding the RadioGroup Architecture
The RadioGroup<T> widget, implemented in packages/flutter/lib/src/widgets/radio_group.dart, serves as an inherited widget that maintains a single groupValue of type T and broadcasts changes through an onChanged callback.
When a Radio<T> or RadioListTile<T> is built inside a RadioGroup, it registers itself using the RadioClient<T> mixin. This registration enables the group to:
- Enforce single-selection: A post-frame check guarantees that only one radio button matches the current
groupValue(see lines 24-31 inradio_group.dart) - Manage focus traversal: The custom
_SkipUnselectedRadioPolicyensures that keyboard navigation (Tab key) lands on the currently selected radio, or the first radio if none is selected (lines 43-55) - Handle keyboard input: Arrow keys and Space/Enter interactions are managed at the group level with proper ARIA semantics
Individual Radio widgets, defined in packages/flutter/lib/src/material/radio.dart, automatically read the nearest RadioGroup context to determine their visual state. Note that the groupValue and onChanged parameters on individual Radio widgets are now deprecated, making RadioGroup the recommended approach for state management.
Setting Up Your Flutter Radio Button Group
To implement a radio button group in your Flutter application:
-
Declare a state variable to hold the selected value (can be nullable to support no selection)
-
Wrap your radio widgets with
RadioGroup<T>, passing the state variable asgroupValueand a callback to update state -
Assign unique values to each
RadioorRadioListTilechild—matching values render as selected
The RadioGroup supports Material Radio<T>, CupertinoRadio<T>, RawRadio<T>, and the convenience wrapper RadioListTile<T>. For toggleable behavior (allowing users to deselect by tapping the selected option), set toggleable: true on individual Radio widgets, which produces a null value in the onChanged callback.
Complete Implementation Examples
Basic Enum-Based Selection
This example demonstrates the standard pattern using RadioListTile with an enum type:
enum Fruit { apple, banana, orange }
class FruitSelector extends StatefulWidget {
const FruitSelector({super.key});
@override
State<FruitSelector> createState() => _FruitSelectorState();
}
class _FruitSelectorState extends State<FruitSelector> {
Fruit? _selectedFruit; // holds the group value
@override
Widget build(BuildContext context) {
return RadioGroup<Fruit>(
groupValue: _selectedFruit,
onChanged: (Fruit? newValue) => setState(() => _selectedFruit = newValue),
child: Column(
children: <Widget>[
RadioListTile<Fruit>(
title: const Text('Apple'),
value: Fruit.apple,
// No need for groupValue/onChanged here – the group handles it
),
RadioListTile<Fruit>(
title: const Text('Banana'),
value: Fruit.banana,
),
RadioListTile<Fruit>(
title: const Text('Orange'),
value: Fruit.orange,
),
],
),
);
}
}
Custom Radio Buttons with Toggleable Options
For custom layouts using the base Radio widget with deselection support:
class ColorChoice extends StatefulWidget {
const ColorChoice({super.key});
@override
State<ColorChoice> createState() => _ColorChoiceState();
}
class _ColorChoiceState extends State<ColorChoice> {
String? _choice; // group value
@override
Widget build(BuildContext context) {
return RadioGroup<String>(
groupValue: _choice,
onChanged: (String? newValue) => setState(() => _choice = newValue),
child: Row(
children: [
Column(
children: [
Radio<String>(value: 'red'),
const Text('Red'),
],
),
Column(
children: [
Radio<String>(value: 'green'),
const Text('Green'),
],
),
Column(
children: [
Radio<String>(value: 'blue', toggleable: true),
const Text('Blue (toggleable)'),
],
),
],
),
);
}
}
Accessing Selection State Programmatically
To read the current selection from a descendant widget in the tree without passing callbacks:
void _showSelection(BuildContext context) {
final selected = RadioGroup.maybeOf<String>(context)?.groupValue;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Selected: ${selected ?? "none"}')),
);
}
Key Features and Accessibility
The flutter/flutter source code provides several built-in optimizations for production applications:
- Semantic wrapping:
RadioGroupautomatically wraps children with properSemanticsnodes for screen reader compatibility - Type safety: The generic
<T>parameter ensures that all radios in a group share the same value type, preventing runtime errors - Performance: State changes trigger minimal rebuilds because individual
Radiowidgets subscribe to the inheritedRadioGrouprather than receiving explicit parameters
Refer to examples/api/lib/widgets/radio_group/radio_group.0.dart in the Flutter repository for a minimal runnable example that demonstrates the public API.
Summary
- Centralize state with
RadioGroup<T>instead of managinggroupValueon individualRadiowidgets - Automatic registration occurs when
RadioorRadioListTilewidgets are descendants of aRadioGroup - Keyboard navigation and focus management are handled internally by the group's
_SkipUnselectedRadioPolicy - Toggleable support allows null values when users deselect the current option
- Deprecated parameters: Avoid using
groupValueandonChangeddirectly onRadiowidgets; these are maintained for backward compatibility only
Frequently Asked Questions
How do I allow users to deselect a radio button in Flutter?
Set the toggleable parameter to true on the specific Radio widget where you want to enable deselection. When the user taps an already-selected toggleable radio, the onChanged callback receives null, allowing you to clear the groupValue in your state management.
What is the difference between Radio and RadioListTile in a RadioGroup?
Radio provides the core circular selection indicator without additional UI elements, while RadioListTile combines a Radio with a title, subtitle, and optional secondary widget in a ListTile layout. Both widgets register with the nearest RadioGroup automatically and read the groupValue from the inherited context.
Why am I getting deprecation warnings on my Radio widgets?
As of recent Flutter versions, the groupValue and onChanged parameters on individual Radio widgets are deprecated in favor of the RadioGroup container. Move these parameters to the RadioGroup ancestor, and remove them from the individual Radio instances to resolve the warnings and follow current best practices.
How does RadioGroup handle accessibility for screen readers?
The RadioGroup widget in packages/flutter/lib/src/widgets/radio_group.dart automatically wraps its children with Semantics nodes that implement proper ARIA radio group semantics. It manages focus traversal so that screen readers announce the group context when users navigate between options using keyboard or gesture navigation.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →