Dart Tutorial

Flutter Lesson 30 of 83 4 min read

Checkbox, Switch, Radio and Slider in Flutter

Learn Flutter selection controls: Checkbox, Switch, radio groups, SegmentedButton and Slider, with list tile versions and state handling.

On this page

These controls let the user choose without typing. They all follow one pattern: you give the control its current value and an onChanged callback, and you update the state yourself.

All of them together #

import 'package:flutter/material.dart';

void main() => runApp(const MaterialApp(home: SettingsPage()));

enum ShirtSize { small, medium, large }

class SettingsPage extends StatefulWidget {
  const SettingsPage({super.key});

  @override
  State<SettingsPage> createState() => _SettingsPageState();
}

class _SettingsPageState extends State<SettingsPage> {
  bool _agree = false;
  bool _notifications = true;
  ShirtSize _size = ShirtSize.medium;
  double _volume = 40;

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Controls')),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          CheckboxListTile(
            title: const Text('I agree to the terms'),
            value: _agree,
            onChanged: (v) => setState(() => _agree = v ?? false),
          ),
          SwitchListTile(
            title: const Text('Notifications'),
            value: _notifications,
            onChanged: (v) => setState(() => _notifications = v),
          ),
          const SizedBox(height: 16),
          SegmentedButton<ShirtSize>(
            segments: const [
              ButtonSegment(value: ShirtSize.small, label: Text('Small')),
              ButtonSegment(value: ShirtSize.medium, label: Text('Medium')),
              ButtonSegment(value: ShirtSize.large, label: Text('Large')),
            ],
            selected: {_size},
            onSelectionChanged: (s) => setState(() => _size = s.first),
          ),
          const SizedBox(height: 16),
          Slider(
            value: _volume,
            min: 0,
            max: 100,
            divisions: 10,
            label: '${_volume.round()}',
            onChanged: (v) => setState(() => _volume = v),
          ),
          Text('Agree: $_agree, notifications: $_notifications, '
              'size: ${_size.name}, volume: ${_volume.round()}'),
        ],
      ),
    );
  }
}

Which control for which choice? #

The user is…Use
Agreeing, or ticking several items in a listCheckbox
Turning a setting on or off, effective at onceSwitch
Choosing one of two to five options, all visibleSegmentedButton or radio buttons
Choosing one of many optionsA dropdown
Choosing a number in a rangeSlider

Checkbox #

Checkbox(
  value: _checked,
  onChanged: (bool? v) => setState(() => _checked = v ?? false),
)

The value is bool? because a checkbox can have a third “partly selected” state when tristate: true. Prefer CheckboxListTile: the whole row becomes tappable.

Switch #

Switch(value: _on, onChanged: (v) => setState(() => _on = v))
Switch.adaptive(value: _on, onChanged: (v) => setState(() => _on = v)) // iOS look on iOS

Radio buttons #

A radio group picks exactly one value. Wrap the options in a RadioGroup, which holds the selected value.

enum Delivery { standard, express, pickup }

Delivery _delivery = Delivery.standard;

RadioGroup<Delivery>(
  groupValue: _delivery,
  onChanged: (v) => setState(() => _delivery = v ?? _delivery),
  child: const Column(
    children: [
      RadioListTile(value: Delivery.standard, title: Text('Standard (3 to 5 days)')),
      RadioListTile(value: Delivery.express, title: Text('Express (next day)')),
      RadioListTile(value: Delivery.pickup, title: Text('Pick up in store')),
    ],
  ),
)

In Flutter versions before RadioGroup existed, each RadioListTile took groupValue and onChanged itself. You will see that form in older code.

SegmentedButton #

A compact alternative to radio buttons. Add multiSelectionEnabled: true to allow several choices, in which case selected holds a set of them.

Slider #

Slider(
  value: _brightness,          // must lie between min and max
  min: 0,
  max: 1,
  onChanged: (v) => setState(() => _brightness = v),
  onChangeEnd: (v) => _save(v), // called once when the user lets go
)

RangeSlider(
  values: _priceRange,          // a RangeValues(100, 500)
  min: 0,
  max: 1000,
  divisions: 20,
  labels: RangeLabels('${_priceRange.start.round()}', '${_priceRange.end.round()}'),
  onChanged: (v) => setState(() => _priceRange = v),
)

Do expensive work, such as saving or a network call, in onChangeEnd, not in onChanged, which fires continuously while dragging.

Disabling a control #

Pass null as onChanged. The control greys out.

Switch(value: _on, onChanged: _canEdit ? (v) => setState(() => _on = v) : null)

A list of checkboxes from data #

final _toppings = <String, bool>{'Cheese': true, 'Olives': false, 'Mushroom': false};

Column(
  children: [
    for (final entry in _toppings.entries)
      CheckboxListTile(
        title: Text(entry.key),
        value: entry.value,
        onChanged: (v) => setState(() => _toppings[entry.key] = v ?? false),
      ),
  ],
)

Try it yourself #

Build a pizza order screen: a segmented button for size, a list of topping checkboxes, a slider for quantity from 1 to 10, and a switch for “extra spicy”. Show a summary line and a total price that update as the choices change.

Practise in the playground Updated by Santosh Adhikari