Switch, Slider & Radio
The Switch Widget
The Switch widget is a Material Design toggle that allows users to turn a setting on or off. It is commonly used for boolean preferences like enabling dark mode, notifications, or Wi-Fi. A Switch has two states: on (true) and off (false).
Basic Switch
class SwitchDemo extends StatefulWidget {
const SwitchDemo({super.key});
@override
State<SwitchDemo> createState() => _SwitchDemoState();
}
class _SwitchDemoState extends State<SwitchDemo> {
bool _isEnabled = false;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Enable Feature'),
Switch(
value: _isEnabled,
onChanged: (bool value) {
setState(() {
_isEnabled = value;
});
},
),
],
);
}
}
Key Switch properties:
value-- Whether the switch is on (true) or off (false).onChanged-- Callback when the user toggles the switch. Set to null to disable.activeColor-- Color of the thumb when the switch is on.activeTrackColor-- Color of the track when the switch is on.inactiveThumbColor-- Color of the thumb when off.inactiveTrackColor-- Color of the track when off.thumbIcon-- An icon displayed on the switch thumb (Material 3).
SwitchListTile
SwitchListTile combines a Switch with a ListTile, providing a full-width tappable area. This is the preferred way to use switches in settings screens.
SwitchListTile Examples
class NotificationSettings extends StatefulWidget {
const NotificationSettings({super.key});
@override
State<NotificationSettings> createState() =>
_NotificationSettingsState();
}
class _NotificationSettingsState
extends State<NotificationSettings> {
bool _pushNotifications = true;
bool _emailNotifications = false;
bool _soundEnabled = true;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SwitchListTile(
title: const Text('Push Notifications'),
subtitle: const Text('Receive push alerts'),
secondary: const Icon(Icons.notifications),
value: _pushNotifications,
onChanged: (value) {
setState(() => _pushNotifications = value);
},
),
const Divider(height: 1),
SwitchListTile(
title: const Text('Email Notifications'),
subtitle: const Text('Get updates via email'),
secondary: const Icon(Icons.email),
value: _emailNotifications,
onChanged: (value) {
setState(() => _emailNotifications = value);
},
),
const Divider(height: 1),
SwitchListTile(
title: const Text('Sound'),
subtitle: const Text('Play notification sounds'),
secondary: const Icon(Icons.volume_up),
value: _soundEnabled,
onChanged: _pushNotifications
? (value) {
setState(() => _soundEnabled = value);
}
: null, // Disabled when push is off
),
],
),
);
}
}
Custom Styled Switch
You can customize the Switch appearance using its color and icon properties:
Styled Switch with Thumb Icon
Switch(
value: _isEnabled,
onChanged: (value) {
setState(() => _isEnabled = value);
},
activeColor: Colors.green,
activeTrackColor: Colors.green[200],
inactiveThumbColor: Colors.grey,
inactiveTrackColor: Colors.grey[300],
thumbIcon: WidgetStateProperty.resolveWith<Icon?>(
(Set<WidgetState> states) {
if (states.contains(WidgetState.selected)) {
return const Icon(Icons.check, color: Colors.white);
}
return const Icon(Icons.close, color: Colors.white);
},
),
)
CupertinoSwitch
For an iOS-style toggle, use CupertinoSwitch from the Cupertino library:
CupertinoSwitch
import 'package:flutter/cupertino.dart';
CupertinoSwitch(
value: _isEnabled,
activeColor: CupertinoColors.activeGreen,
onChanged: (bool value) {
setState(() {
_isEnabled = value;
});
},
)
The Slider Widget
The Slider widget lets users select a value from a continuous or discrete range by dragging a thumb along a track. Common uses include volume controls, brightness settings, and filter ranges.
Basic Slider
class SliderDemo extends StatefulWidget {
const SliderDemo({super.key});
@override
State<SliderDemo> createState() => _SliderDemoState();
}
class _SliderDemoState extends State<SliderDemo> {
double _volume = 50;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
'Volume: \${_volume.round()}',
style: const TextStyle(fontSize: 18),
),
Slider(
value: _volume,
min: 0,
max: 100,
onChanged: (double value) {
setState(() {
_volume = value;
});
},
),
],
);
}
}
Key Slider properties:
value-- The current value of the slider.min/max-- The minimum and maximum values (defaults: 0.0 and 1.0).divisions-- Number of discrete divisions. If null, the slider is continuous.label-- A label displayed above the thumb when using divisions.onChanged-- Called continuously as the user drags the slider.onChangeStart-- Called when the user starts dragging.onChangeEnd-- Called when the user stops dragging.activeColor-- Color of the active portion of the track (before the thumb).inactiveColor-- Color of the inactive portion (after the thumb).
Discrete Slider with Divisions
Adding divisions makes the slider snap to discrete values and shows a value indicator label:
Discrete Slider
class FontSizeSlider extends StatefulWidget {
const FontSizeSlider({super.key});
@override
State<FontSizeSlider> createState() => _FontSizeSliderState();
}
class _FontSizeSliderState extends State<FontSizeSlider> {
double _fontSize = 16;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
'Preview Text',
style: TextStyle(fontSize: _fontSize),
),
const SizedBox(height: 16),
Slider(
value: _fontSize,
min: 10,
max: 36,
divisions: 26,
label: '\${_fontSize.round()} pt',
onChanged: (double value) {
setState(() {
_fontSize = value;
});
},
),
Text('Font Size: \${_fontSize.round()} pt'),
],
);
}
}
label property only appears when divisions is set. It shows a floating indicator above the thumb displaying the current value, which is very helpful for discrete selection.RangeSlider
The RangeSlider allows users to select a range of values by providing two thumbs:
RangeSlider Example
class PriceRangeFilter extends StatefulWidget {
const PriceRangeFilter({super.key});
@override
State<PriceRangeFilter> createState() =>
_PriceRangeFilterState();
}
class _PriceRangeFilterState extends State<PriceRangeFilter> {
RangeValues _priceRange = const RangeValues(20, 80);
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Price: \$\${_priceRange.start.round()} - '
'\$\${_priceRange.end.round()}',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
RangeSlider(
values: _priceRange,
min: 0,
max: 100,
divisions: 20,
labels: RangeLabels(
'\$\${_priceRange.start.round()}',
'\$\${_priceRange.end.round()}',
),
onChanged: (RangeValues values) {
setState(() {
_priceRange = values;
});
},
),
],
);
}
}
CupertinoSlider
For an iOS-style slider, use CupertinoSlider:
CupertinoSlider
import 'package:flutter/cupertino.dart';
CupertinoSlider(
value: _volume,
min: 0,
max: 100,
divisions: 100,
onChanged: (double value) {
setState(() {
_volume = value;
});
},
)
The Radio Widget
The Radio widget allows users to select one option from a set of mutually exclusive choices. All Radio buttons in a group share the same groupValue, and only one can be selected at a time.
Basic Radio Group
class GenderSelection extends StatefulWidget {
const GenderSelection({super.key});
@override
State<GenderSelection> createState() => _GenderSelectionState();
}
class _GenderSelectionState extends State<GenderSelection> {
String _selectedGender = 'male';
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Gender',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Row(
children: [
Radio<String>(
value: 'male',
groupValue: _selectedGender,
onChanged: (String? value) {
setState(() {
_selectedGender = value!;
});
},
),
const Text('Male'),
Radio<String>(
value: 'female',
groupValue: _selectedGender,
onChanged: (String? value) {
setState(() {
_selectedGender = value!;
});
},
),
const Text('Female'),
Radio<String>(
value: 'other',
groupValue: _selectedGender,
onChanged: (String? value) {
setState(() {
_selectedGender = value!;
});
},
),
const Text('Other'),
],
),
],
);
}
}
Key Radio properties:
value-- The value this Radio represents.groupValue-- The currently selected value in the group. Whenvalue == groupValue, this Radio is selected.onChanged-- Callback when this Radio is selected.activeColor-- Color when selected.toggleable-- If true, tapping the selected radio deselects it (sets groupValue to null).
RadioListTile
RadioListTile combines a Radio button with a ListTile for a larger tappable area and better layout:
RadioListTile Example
class ThemeSelector extends StatefulWidget {
const ThemeSelector({super.key});
@override
State<ThemeSelector> createState() => _ThemeSelectorState();
}
class _ThemeSelectorState extends State<ThemeSelector> {
String _selectedTheme = 'system';
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'App Theme',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
RadioListTile<String>(
title: const Text('Light'),
subtitle: const Text('Always use light theme'),
secondary: const Icon(Icons.light_mode),
value: 'light',
groupValue: _selectedTheme,
onChanged: (value) {
setState(() => _selectedTheme = value!);
},
),
RadioListTile<String>(
title: const Text('Dark'),
subtitle: const Text('Always use dark theme'),
secondary: const Icon(Icons.dark_mode),
value: 'dark',
groupValue: _selectedTheme,
onChanged: (value) {
setState(() => _selectedTheme = value!);
},
),
RadioListTile<String>(
title: const Text('System'),
subtitle: const Text('Follow device setting'),
secondary: const Icon(Icons.settings_suggest),
value: 'system',
groupValue: _selectedTheme,
onChanged: (value) {
setState(() => _selectedTheme = value!);
},
),
],
),
);
}
}
Using Enums with Radio
Using enums with Radio widgets is a cleaner and type-safe approach:
Radio with Enums
enum ShippingMethod { standard, express, overnight }
class ShippingSelector extends StatefulWidget {
const ShippingSelector({super.key});
@override
State<ShippingSelector> createState() =>
_ShippingSelectorState();
}
class _ShippingSelectorState extends State<ShippingSelector> {
ShippingMethod _method = ShippingMethod.standard;
String _getLabel(ShippingMethod method) {
switch (method) {
case ShippingMethod.standard:
return 'Standard (5-7 days)';
case ShippingMethod.express:
return 'Express (2-3 days)';
case ShippingMethod.overnight:
return 'Overnight (next day)';
}
}
String _getPrice(ShippingMethod method) {
switch (method) {
case ShippingMethod.standard:
return 'Free';
case ShippingMethod.express:
return '\$9.99';
case ShippingMethod.overnight:
return '\$19.99';
}
}
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: ShippingMethod.values.map((method) {
return RadioListTile<ShippingMethod>(
title: Text(_getLabel(method)),
subtitle: Text(_getPrice(method)),
value: method,
groupValue: _method,
onChanged: (value) {
setState(() => _method = value!);
},
);
}).toList(),
),
);
}
}
Practical Example: Settings Page
Complete Settings Page
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
bool _darkMode = false;
bool _notifications = true;
double _fontSize = 16;
String _language = 'en';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Settings')),
body: ListView(
children: [
// Toggle section
const _SectionHeader(title: 'General'),
SwitchListTile(
secondary: const Icon(Icons.dark_mode),
title: const Text('Dark Mode'),
value: _darkMode,
onChanged: (v) => setState(() => _darkMode = v),
),
SwitchListTile(
secondary: const Icon(Icons.notifications),
title: const Text('Notifications'),
value: _notifications,
onChanged: (v) => setState(() => _notifications = v),
),
// Slider section
const _SectionHeader(title: 'Display'),
ListTile(
leading: const Icon(Icons.text_fields),
title: const Text('Font Size'),
subtitle: Slider(
value: _fontSize,
min: 12,
max: 24,
divisions: 6,
label: '\${_fontSize.round()} pt',
onChanged: (v) => setState(() => _fontSize = v),
),
trailing: Text('\${_fontSize.round()}'),
),
// Radio section
const _SectionHeader(title: 'Language'),
RadioListTile<String>(
title: const Text('English'),
value: 'en',
groupValue: _language,
onChanged: (v) => setState(() => _language = v!),
),
RadioListTile<String>(
title: const Text('Arabic'),
value: 'ar',
groupValue: _language,
onChanged: (v) => setState(() => _language = v!),
),
RadioListTile<String>(
title: const Text('Spanish'),
value: 'es',
groupValue: _language,
onChanged: (v) => setState(() => _language = v!),
),
],
),
);
}
}
class _SectionHeader extends StatelessWidget {
final String title;
const _SectionHeader({required this.title});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.grey[600],
),
),
);
}
}
Practical Example: Filter Controls
Product Filter Panel
class FilterPanel extends StatefulWidget {
const FilterPanel({super.key});
@override
State<FilterPanel> createState() => _FilterPanelState();
}
class _FilterPanelState extends State<FilterPanel> {
RangeValues _priceRange = const RangeValues(10, 90);
double _rating = 3;
String _sortBy = 'relevance';
bool _inStock = true;
bool _onSale = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Filters')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
// Price range
const Text(
'Price Range',
style: TextStyle(fontWeight: FontWeight.bold),
),
Text(
'\$\${_priceRange.start.round()} - '
'\$\${_priceRange.end.round()}',
),
RangeSlider(
values: _priceRange,
min: 0,
max: 100,
divisions: 20,
labels: RangeLabels(
'\$\${_priceRange.start.round()}',
'\$\${_priceRange.end.round()}',
),
onChanged: (v) => setState(() => _priceRange = v),
),
const SizedBox(height: 16),
// Minimum rating
const Text(
'Minimum Rating',
style: TextStyle(fontWeight: FontWeight.bold),
),
Row(
children: [
Expanded(
child: Slider(
value: _rating,
min: 1,
max: 5,
divisions: 4,
label: '\${_rating.round()} stars',
onChanged: (v) => setState(() => _rating = v),
),
),
Text('\${_rating.round()} stars'),
],
),
const SizedBox(height: 16),
// Sort options
const Text(
'Sort By',
style: TextStyle(fontWeight: FontWeight.bold),
),
RadioListTile<String>(
title: const Text('Relevance'),
value: 'relevance',
groupValue: _sortBy,
onChanged: (v) => setState(() => _sortBy = v!),
dense: true,
),
RadioListTile<String>(
title: const Text('Price: Low to High'),
value: 'price_asc',
groupValue: _sortBy,
onChanged: (v) => setState(() => _sortBy = v!),
dense: true,
),
RadioListTile<String>(
title: const Text('Price: High to Low'),
value: 'price_desc',
groupValue: _sortBy,
onChanged: (v) => setState(() => _sortBy = v!),
dense: true,
),
const SizedBox(height: 16),
// Toggle filters
SwitchListTile(
title: const Text('In Stock Only'),
value: _inStock,
onChanged: (v) => setState(() => _inStock = v),
),
SwitchListTile(
title: const Text('On Sale'),
value: _onSale,
onChanged: (v) => setState(() => _onSale = v),
),
],
),
bottomNavigationBar: Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: () {
debugPrint('Applying filters...');
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 16),
),
child: const Text('Apply Filters'),
),
),
);
}
}
Expanded or give it a fixed width. Without constraints, the Slider will try to take infinite width and cause a layout error.Practical Example: Preference Form
User Preference Form
class PreferenceForm extends StatefulWidget {
const PreferenceForm({super.key});
@override
State<PreferenceForm> createState() => _PreferenceFormState();
}
class _PreferenceFormState extends State<PreferenceForm> {
bool _newsletter = true;
bool _autoplay = false;
double _brightness = 0.7;
String _quality = 'hd';
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Preferences')),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: Column(
children: [
SwitchListTile(
secondary: const Icon(Icons.mail),
title: const Text('Newsletter'),
subtitle: const Text('Weekly updates'),
value: _newsletter,
onChanged: (v) =>
setState(() => _newsletter = v),
),
const Divider(height: 1),
SwitchListTile(
secondary: const Icon(Icons.play_arrow),
title: const Text('Autoplay Videos'),
value: _autoplay,
onChanged: (v) =>
setState(() => _autoplay = v),
),
],
),
),
const SizedBox(height: 16),
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
const Text(
'Brightness',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
Text(
'\${(_brightness * 100).round()}%',
),
],
),
Slider(
value: _brightness,
onChanged: (v) =>
setState(() => _brightness = v),
),
],
),
),
),
const SizedBox(height: 16),
Card(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.all(16),
child: Text(
'Video Quality',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
RadioListTile<String>(
title: const Text('Auto'),
subtitle: const Text(
'Adjusts based on connection'),
value: 'auto',
groupValue: _quality,
onChanged: (v) =>
setState(() => _quality = v!),
),
RadioListTile<String>(
title: const Text('HD (720p)'),
value: 'hd',
groupValue: _quality,
onChanged: (v) =>
setState(() => _quality = v!),
),
RadioListTile<String>(
title: const Text('Full HD (1080p)'),
value: 'fhd',
groupValue: _quality,
onChanged: (v) =>
setState(() => _quality = v!),
),
],
),
),
],
),
);
}
}
Practice Exercise
Build a complete preference screen with all three widget types: (1) A Switch section with three SwitchListTiles where the second and third toggles are disabled when the first is off. (2) A Slider section with both a regular Slider (for a single value like volume) and a RangeSlider (for a range like price filter), both with divisions and labels. (3) A Radio section using enums with RadioListTile for selecting a theme (Light, Dark, System). (4) An "Apply" button at the bottom that prints all current values. Challenge: Use CupertinoSwitch for one toggle and add a custom styled Slider with SliderTheme.