This tutorial shows you how to create a radio group using the Radio
widget in Flutter.
If you have a list of options where a user can only select one of them at the same time, it's quite common to create a radio group consisting of several radio buttons. A radio button usually uses an empty outer circle icon, with a solid inner circle if it's currently being selected. In Flutter, you can use the Radio
widget to create a radio button. The usage is quite simple and you can also customize the button.
Using Radio
Below is the constructor to use.
const Radio({
Key? key,
required dynamic value,
required dynamic groupValue,
required ValueChanged<T?>? onChanged,
MouseCursor? mouseCursor,
bool toggleable = false,
Color? activeColor,
MaterialStateProperty<Color?>? fillColor,
Color? focusColor,
Color? hoverColor,
MaterialStateProperty<Color?>? overlayColor,
double? splashRadius,
MaterialTapTargetSize? materialTapTargetSize,
VisualDensity? visualDensity,
FocusNode? focusNode,
bool autofocus = false,
})
As you can see from the constructor, the required arguments are value
, groupValue
, and onChanged
. The other arguments are optional and can be used to change the style and behavior of the radio button.
Set Value and Group Value
A radio button must have a value passed as the value
argument. To specify what value is currently being selected in a group, you have to pass the groupValue
argument. A radio button is in selected state if its value
matches the groupValue
. Since a group can only have one selected value, the selected group value must be the same across all radio buttons in a group. Therefore, it's recommended to pass the same variable (e.g. using a state variable) as the groupValue
argument, so that you only need to update a variable for changing the currently selected value. If you want to set one of the buttons to be selected initially, just set the button's value
as the initial value of the variable passed as the groupValue
. In almost all cases, the value
must be unique among the buttons in the same group, unless you allow more than one radio button selected at the same time — which is unusual.
The currently selected value can be changed when the user clicks on a radio button. You have to handle that event inside a callback function passed as onChanged
argument. If you use a state variable to store the selected value, you need to update it.
In this tutorial, we are going to create a radio group whose value type is integer. First, create a state variable for the group value.
int? _groupValue;
The Radio
widget only creates and handles the radio button. If you want to add a text, you need to add it separately. For example, you can use the ListTile
widget and pass a Text
widget as the title
argument and a Radio
as the leading
argument. Below is a method for creating a radio option, including the button and the text. For using the Radio
widget, make sure you pass all the required arguments.
Widget _buildItem(String text, int value) {
return ListTile(
title: Text(text),
leading: Radio<int>(
groupValue: _groupValue,
value: value,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
);
}
Output:
Set Colors
Flutter allows you to change the button color. When the button is in the selected state, you can set the color by passing the activeColor
argument. When the button has the current focus, you can set a different color by passing the focusColor
argument. You can also set the color when a pointer is hovering over the button (on the web browser) using the hoverColor
argument. The color set as the activeColor
affects both the empty (outer) and the solid (inner) circles. Meanwhile, the colors set as the focusColor
and hoverColor
affect the entire button other than the empty and the solid circles. In other words, it affects the background color of the button, which is usually referred to as Material
ink splash. The splash is only shown in certain states such as focused
and hovered
.
Widget _buildItem(String text, int value) {
return ListTile(
title: Text(text),
leading: Radio<int>(
groupValue: _groupValue,
value: value,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
hoverColor: Colors.yellow,
activeColor: Colors.pink,
focusColor: Colors.purple,
// other arguments
);
}
Output:
Another way to set the colors is using the fillColor
argument whose type is MaterialStateProperty
. You can determine the color to use based on the current state. fillColor
only sets the color for the outer and inner circles. Be careful that using fillColor
overrides the color passed as the activeColor
argument.
Widget _buildItem(String text, int value) {
return ListTile(
title: Text(text),
leading: Radio<int>(
groupValue: _groupValue,
value: value,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
hoverColor: Colors.yellow,
activeColor: Colors.pink,
focusColor: Colors.purple,
fillColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return Colors.orange;
} else if (states.contains(MaterialState.selected)) {
return Colors.teal;
} if (states.contains(MaterialState.focused)) {
return Colors.blue;
} else {
return Colors.black12;
}
}),
// other arguments
);
}
Output:
Another argument for customizing the colors is overlayColor
. The usage is similar to fillColor
, which allows you to determine the color based on the current state. The difference is overlayColor
is used for setting the Material
ink splash rather than the color of the circles. Be careful that it will override the colors set as focusColor
and hoverColor
arguments if you pass any of them.
Widget _buildItem(String text, int value) {
return ListTile(
title: Text(text),
leading: Radio<int>(
groupValue: _groupValue,
value: value,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
hoverColor: Colors.yellow,
activeColor: Colors.pink,
focusColor: Colors.purple,
overlayColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return Colors.lightGreenAccent;
} if (states.contains(MaterialState.focused)) {
return Colors.brown;
} else {
return Colors.white;
}
}),
// other arguments
);
}
Output:
Set Splash Radius
The Material
ink splash of a button is shown in certain states which include hovered
or focused
. You can change the radius of the splash by passing a double
value as the splashRadius
argument.
Widget _buildItem(String text, int value) {
return ListTile(
title: Text(text),
leading: Radio<int>(
groupValue: _groupValue,
value: value,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
overlayColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return Colors.lightGreenAccent;
} if (states.contains(MaterialState.focused)) {
return Colors.brown;
} else {
return Colors.white;
}
}),
splashRadius: 25,
// other arguments
);
}
Output:
Allow/Disallow Toggle
In order to unselect a radio button, usually the user needs to select another one. If you want to allow a radio button to be unselected without requiring the user to select another one, you can pass the toggleable
argument and set the value to true
. The value defaults to false
if you don't pass it.
Widget _buildItem(String text, int value) {
return ListTile(
title: Text(text),
leading: Radio<int>(
groupValue: _groupValue,
value: value,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
toggleable: true,
// other arguments
);
}
Output:
Set Visual Density
You can change the density of the layout by using the visualDensity
argument. The value you need to pass is a VisualDensity
. You can use one of the static constants or create your own custom VisualDensity
Widget _buildItem(String text, int value) {
return ListTile(
title: Text(text),
leading: Radio<int>(
groupValue: _groupValue,
value: value,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
visualDensity: VisualDensity.compact,
// other arguments
);
}
Output:
Set Minimum Tap Target Size
Ideally, radio& buttons should have a minimum tap target size so that the users can comfortably select a particular option. You can change it by passing a MaterialTapTargetSize
enum as the materialTapTargetSize
argument.
Widget _buildItem(String text, int value) {
return ListTile(
title: Text(text),
leading: Radio<int>(
groupValue: _groupValue,
value: value,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
// other arguments
);
}
Set Focus Node
The constructor has an argument named focusNode
which allows you to pass a FocusNode
to use for the widget. In the example below, we are going to set a custom FocusNode
for each button, with the first button has the current focus. First, create the FocusNode
s and set the focus to the first one.
late List<FocusNode> _focusNodes;
@override
void initState() {
_focusNodes = Iterable<int>.generate(3)
.map((e) => FocusNode())
.toList();
_focusNodes[0].requestFocus();
}
Then, modify the _buildItem
method above to accept a FocusNode
which will be passed as the focusNode
argument.
Widget _buildItem(String text, int value, FocusNode focusNode) {
return ListTile(
title: Text(text),
leading: Radio<int>(
groupValue: _groupValue,
value: value,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
focusNode: focusNode,
// other arguments
),
);
}
Radio
- Parameters
Key? key
: The widget's key, used to control how a widget is replaced with another widget.required dynamic value
: The value for the radio button.required dynamic groupValue
: The current value for a group of radio buttons.required ValueChanged<T?>? onChanged
: A callback function that will be when the user selects the radio button.MouseCursor? mouseCursor
: The cursor for a mouse pointer when it enters or is hovering over the widget on the web. If null,RadioThemeData.mouseCursor
is used. IfRadioThemeData.mouseCursor
is also null,MaterialStateMouseCursor.clickable
is used.bool toggleable
: yyy. Defaults tofalse
.Color? activeColor
: The color to use when this radio button is selected.MaterialStateProperty<Color?>? fillColor
: The color that fills the radio button which can be differentiated for eachMaterialState
s.Color? focusColor
: The color for the radio'sMaterial
when it has the input focus.Color? hoverColor
: The color for the radio'sMaterial
when a pointer is hovering over it.MaterialStateProperty<Color?>? overlayColor
: The color of the button'sMaterial
.double? splashRadius
: The splash radius of the circularMaterial
ink response.MaterialTapTargetSize? MaterialTapTargetSize
: Configures the minimum size of the tap target.VisualDensity? visualDensity
: The compactness of the layout.FocusNode? focusNode
: The focus node to use for the widget.bool autofocus
: Whether it should be selected as the initial focus when no other node in its scope is currently focused. Defaults tofalse
.
?: value can be null.
required: value must be passed.
Full Code
import 'package:flutter/material.dart';
void main() => runApp(RadioExample());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Woolha.com Flutter Tutorial',
home: RadioExample(),
);
}
}
class RadioExample extends StatefulWidget {
@override
State createState() => new _RadioExampleState();
}
class _RadioExampleState extends State<RadioExample> {
int? _groupValue;
late List<FocusNode> _focusNodes;
@override
void initState() {
_focusNodes = Iterable<int>.generate(3)
.map((e) => FocusNode())
.toList();
_focusNodes[0].requestFocus();
}
Widget _buildItem(String text, int value, FocusNode focusNode) {
return ListTile(
title: Text(text),
leading: Radio<int>(
groupValue: _groupValue,
value: value,
onChanged: (int? value) {
setState(() {
_groupValue = value;
});
},
hoverColor: Colors.yellow,
activeColor: Colors.pink,
focusColor: Colors.purple,
fillColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return Colors.orange;
} else if (states.contains(MaterialState.selected)) {
return Colors.teal;
} if (states.contains(MaterialState.focused)) {
return Colors.blue;
} else {
return Colors.black12;
}
}),
overlayColor: MaterialStateColor.resolveWith((Set<MaterialState> states) {
if (states.contains(MaterialState.hovered)) {
return Colors.lightGreenAccent;
} if (states.contains(MaterialState.focused)) {
return Colors.brown;
} else {
return Colors.white;
}
}),
splashRadius: 25,
toggleable: true,
visualDensity: VisualDensity.compact,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
focusNode: focusNode,
),
);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: const Text('Woolha.com Flutter Tutorial'),
),
body: Column(
children: [
_buildItem("One", 1, _focusNodes[0]),
_buildItem("Two", 2, _focusNodes[1]),
_buildItem("Three", 3, _focusNodes[2]),
],
),
);
}
}
Summary
That's how to use the Radio
widget in Flutter. There are three required arguments that must be passed — value
, groupValue
, and onChanged
. You can also pass the other arguments to change the style and behavior. If you want to create something with similar functionality to a radio group, but without using the conventional radio button icon, it's recommended to read our tutorial about creating custom radio button in Flutter.