Radio Button in Flutter: Radio, ListTile & RadioListTile Tutorial (2026)
Last Updated: | Author: Deep Singh
Radio buttons are how users pick one option out of several. You've seen them in signup forms (choose your gender), settings screens (choose your theme), and checkout pages (choose your payment method). They're simple, familiar, and easy to get right in Flutter.
In this tutorial, you'll learn the three ways to build radio buttons in Flutter: using Radio on its own, wrapping it in a ListTile for a nice row layout, and using the purpose-built RadioListTile widget. We'll build the same gender selection form three different ways so you can see the trade-offs.
How Radio Buttons Work in Flutter
Every radio button has three key properties:
- value — the value this particular button represents (e.g.,
Gender.Male). - groupValue — the currently selected value for the group. If
value == groupValue, this button is shown as selected. - onChanged — a callback that fires when the user taps the button. You update the state here.
All radios in the same group share the same groupValue. When you tap a button, you set groupValue to that button's value, and Flutter re-renders the group with the new selection. Only one can be selected at a time because only one value can equal groupValue.
Using an enum for the values is a common pattern — it's type-safe and makes your intent clear in the code.
The Full Code
Here's a complete working example. It shows two approaches: a Radio widget wrapped in a ListTile, and a RadioListTile which combines both into one widget.
import 'package:flutter/material.dart';
enum Gender { male, female }
void main() {
runApp(const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Radio Button In Flutter',
home: MyRadioPage(),
));
}
class MyRadioPage extends StatefulWidget {
const MyRadioPage({super.key});
@override
State<MyRadioPage> createState() => _MyRadioPageState();
}
class _MyRadioPageState extends State<MyRadioPage> {
Gender _selectedGender = Gender.male;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Radio Button'),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// ---------- Approach 1: Radio + ListTile ----------
const Text(
'Radio with ListTile',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
ListTile(
leading: Radio<Gender>(
value: Gender.male,
groupValue: _selectedGender,
onChanged: _onGenderChanged,
),
title: const Text('Male'),
onTap: () => _onGenderChanged(Gender.male),
),
ListTile(
leading: Radio<Gender>(
value: Gender.female,
groupValue: _selectedGender,
onChanged: _onGenderChanged,
),
title: const Text('Female'),
onTap: () => _onGenderChanged(Gender.female),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
debugPrint('Selected: ${_selectedGender.name}');
},
child: const Text('Submit Me'),
),
const Divider(height: 40),
// ---------- Approach 2: RadioListTile ----------
const Text(
'Radio with RadioListTile',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
RadioListTile<Gender>(
value: Gender.male,
groupValue: _selectedGender,
onChanged: _onGenderChanged,
title: const Text('Male'),
),
RadioListTile<Gender>(
value: Gender.female,
groupValue: _selectedGender,
onChanged: _onGenderChanged,
title: const Text('Female'),
),
const SizedBox(height: 10),
ElevatedButton(
onPressed: () {
debugPrint('Selected: ${_selectedGender.name}');
},
child: const Text('Submit Me'),
),
],
),
),
);
}
void _onGenderChanged(Gender? value) {
if (value == null) return;
setState(() {
_selectedGender = value;
});
}
}
How the Code Works
Let's walk through the important parts:
1. Using an Enum for Values
enum Gender { male, female }
Instead of storing strings like "male" and "female", we define an enum. This gives us type safety (you can't accidentally assign "other" somewhere) and makes the code easier to read and refactor.
2. The Shared State
Gender _selectedGender = Gender.male;
This single variable holds the currently selected value for the whole group. Every radio button reads this value through groupValue, and only the button whose value matches will appear selected.
3. The onChanged Callback
void _onGenderChanged(Gender? value) {
if (value == null) return;
setState(() {
_selectedGender = value;
});
}
When the user taps a radio, this callback fires with the new value. We call setState() to rebuild the widget with the new selection. The null check is important — onChanged can receive null in some cases (like when the radio is disabled), and assigning that to _selectedGender would cause a crash if it were non-nullable.
4. Radio + ListTile
Wrapping a Radio in a ListTile gives you the nice row layout — icon on the left, text next to it, and proper Material spacing. We also add onTap to the ListTile so tapping anywhere on the row (not just the radio) selects the option. That's a small UX improvement that users will appreciate.
5. RadioListTile
This widget combines Radio and ListTile into one. It handles the layout and tap behavior automatically, so you don't need a separate onTap on the tile. Use it when you want the standard Material look with less boilerplate.
Which One Should You Use?
Here's a quick decision guide:
- Use
RadioListTilefor the standard Material row with a radio and a label. It's the cleanest option and handles tap-anywhere-on-the-row for you. - Use
Radio+ListTilewhen you need to add something extra to the row — a subtitle, a trailing icon, a description underneath the label, or custom padding. The extra flexibility is worth the small bit of boilerplate. - Use bare
Radiowhen you're placing it in a custom layout that isn't a list row — for instance, side by side in a horizontal row.
A couple of extra tips:
- Add
onTapto your ListTile if you go with the second approach. Without it, users have to tap the tiny radio circle itself, which is annoying. - Don't use
selected: trueon every RadioListTile. Theselectedproperty just changes the text colour, not the selection state. The actual selection is driven bygroupValue. Setting it totrueon all of them makes every label look highlighted regardless of which is selected. - Consider a
SegmentedButtonfor two or three options on modern apps. Flutter'sSegmentedButton(added in Flutter 3.7) is often a better fit for short option lists and looks more contemporary than a stack of radio tiles.
Wrapping Up
You now know the three ways to build radio button groups in Flutter: bare Radio, Radio inside ListTile, and RadioListTile. Use RadioListTile by default, and drop down to the more manual options only when you need extra customisation.
From here, you can combine radio buttons with a Form and validation (see the TextField tutorial) or extend them into more complex widgets like checkboxes for multi-select or switches for binary on/off settings.
No comments:
Post a Comment