Flutter - Creating Custom Radio Button Examples

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.

Flutter - Custom Radio 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.