This tutorial shows you how to create a custom radio button in Flutter.
Flutter allows us to create radio buttons by using the Radio
widget. A radio button created with that widget consists of an empty outer circle and a solid inner circle, with the latter only shown in selected state. In some cases, you may want to create a radio group whose options use a custom design, not the conventional radio button. This tutorial gives an example of how to create a radio group with custom buttons.
Creating Custom Radio Button
In this tutorial, we are going to create a radio group. Each option has a radio button with a label on the left side. There is also a text next to the button.
Since the radio button contains a label, we can't use the Radio
button. Instead, we are going to create a custom class that can be used the create the options called MyRadioOption
. Inspired by Flutter's Radio
widget, the class has value
, groupValue
, and onChanged
properties. The value
property represents the value of the option, it should be unique among all options in the same group. The groupValue
property is the currently selected value. If an option's value
matches the groupValue
, the option is in selected state. The onChanged
property stores the callback function that will be called when the user selects an option. When a user selects an option, it's the responsibility of the callback function to update the groupValue
. In addition, we also add label
and text
properties because we need to display a label on the button and a text on the right side of the button.
Below are the properties and the constructor of the class. We use a generic type T
because the value can be any type.
class MyRadioOption<T> extends StatelessWidget {
final T value;
final T? groupValue;
final String label;
final String text;
final ValueChanged<T?> onChanged;
const MyRadioOption({
required this.value,
required this.groupValue,
required this.label,
required this.text,
required this.onChanged,
});
@override
Widget build(BuildContext context) {
// TODO implement
}
}
Next, we are going to create the layout. The button is a circle with a label inside. For creating the circle, create a Container
that uses a ShapeDecoration
with a CircleBorder
as the shape
. The label can be created using a Text
widget which is placed as the child of the Container
. Then, we can create a Row
that consists of the button and a Text
widget.
The options must be selectable. That means the options need to be able to detect tap gestures. There are some widgets that can be used to detect tap gestures, such as Listener
, GestureDetector
, and InkWell
. In this tutorial, we are going to use InkWell
because of its ability to create effects in response to a touch. To detect tap gestures using InkWell
, pass a callback function as the onTap
argument. In this case, the callback passed as onChanged
needs to be invoked when a tap gesture is detected by the InkWell
class MyRadioOption<T> extends StatelessWidget {
final T value;
final T? groupValue;
final String label;
final String text;
final ValueChanged<T?> onChanged;
const MyRadioOption({
required this.value,
required this.groupValue,
required this.label,
required this.text,
required this.onChanged,
});
Widget _buildLabel() {
final bool isSelected = value == groupValue;
return Container(
width: 50,
height: 50,
decoration: ShapeDecoration(
shape: CircleBorder(
side: BorderSide(
color: Colors.black,
),
),
color: isSelected ? Colors.teal : Colors.white,
),
child: Center(
child: Text(
value.toString(),
style: TextStyle(
color: isSelected ? Colors.white : Colors.teal,
fontSize: 24,
),
),
),
);
}
Widget _buildText() {
return Text(
text,
style: const TextStyle(color: Colors.black, fontSize: 24),
);
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(5),
child: InkWell(
onTap: () => onChanged(value),
splashColor: Colors.teal.withOpacity(0.5),
child: Padding(
padding: EdgeInsets.all(5),
child: Row(
children: [
_buildLabel(),
const SizedBox(width: 10),
_buildText(),
],
),
),
),
);
}
}
Below is a class in which we create a radio group using the MyRadioOption
as the options. There is a state variable _groupValue
and a ValueChanged
function which is the callback to be called when the user selects an option.
class _CustomRadioExampleState extends State<CustomRadioExample> {
String? _groupValue;
ValueChanged<String?> _valueChangedHandler() {
return (value) => setState(() => _groupValue = value!);
}
}
And below is an example of how to call the constructor of MyRadioOption
.
MyRadioOption<String>(
value: 'A',
groupValue: _groupValue,
onChanged: _valueChangedHandler(),
label: 'A',
text: 'One',
)
Full Code
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Woolha.com Flutter Tutorial',
home: CustomRadioExample(),
);
}
}
class MyRadioOption<T> extends StatelessWidget {
final T value;
final T? groupValue;
final String label;
final String text;
final ValueChanged<T?> onChanged;
const MyRadioOption({
required this.value,
required this.groupValue,
required this.label,
required this.text,
required this.onChanged,
});
Widget _buildLabel() {
final bool isSelected = value == groupValue;
return Container(
width: 50,
height: 50,
decoration: ShapeDecoration(
shape: CircleBorder(
side: BorderSide(
color: Colors.black,
),
),
color: isSelected ? Colors.teal : Colors.white,
),
child: Center(
child: Text(
value.toString(),
style: TextStyle(
color: isSelected ? Colors.white : Colors.teal,
fontSize: 24,
),
),
),
);
}
Widget _buildText() {
return Text(
text,
style: const TextStyle(color: Colors.black, fontSize: 24),
);
}
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.all(5),
child: InkWell(
onTap: () => onChanged(value),
splashColor: Colors.teal.withOpacity(0.5),
child: Padding(
padding: EdgeInsets.all(5),
child: Row(
children: [
_buildLabel(),
const SizedBox(width: 10),
_buildText(),
],
),
),
),
);
}
}
class CustomRadioExample extends StatefulWidget {
@override
State createState() => new _CustomRadioExampleState();
}
class _CustomRadioExampleState extends State<CustomRadioExample> {
String? _groupValue;
ValueChanged<String?> _valueChangedHandler() {
return (value) => setState(() => _groupValue = value!);
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: const Text('Woolha.com Flutter Tutorial'),
backgroundColor: Colors.teal,
),
body: Column(
children: [
MyRadioOption<String>(
value: 'A',
groupValue: _groupValue,
onChanged: _valueChangedHandler(),
label: 'A',
text: 'One',
),
MyRadioOption<String>(
value: 'B',
groupValue: _groupValue,
onChanged: _valueChangedHandler(),
label: 'B',
text: 'Two',
),
MyRadioOption<String>(
value: 'C',
groupValue: _groupValue,
onChanged: _valueChangedHandler(),
label: 'C',
text: 'Three',
),
MyRadioOption<String>(
value: 'D',
groupValue: _groupValue,
onChanged: _valueChangedHandler(),
label: 'D',
text: 'Four',
),
MyRadioOption<String>(
value: 'E',
groupValue: _groupValue,
onChanged: _valueChangedHandler(),
label: 'E',
text: 'Five',
),
],
),
);
}
}
Summary
That's an example of how to create custom radio buttons. Basically, each option must have a value and a group value. The group value must be the same among all options in the same group. The group value is updated when a user selects an option. If you have understood the concept, you should be able to create your own custom radio buttons. If the button design is not simple and not easy to be created programmatically, you can consider using custom icons.