3.4 单选开关和复选框
📚 章节概览
选择组件是用户界面中常见的交互元素。Flutter 提供了 Material 风格的选择组件,本章节将学习:
- Switch - 单选开关(开/关)
- Checkbox - 复选框(多选)
- Radio - 单选按钮(单选)
- CheckboxListTile - 带标题的复选框
- SwitchListTile - 带标题的开关
- RadioListTile - 带标题的单选按钮
🎯 核心知识点
重要特性
所有选择组件都有以下共同特点:
- 状态管理:组件本身不保存状态,状态由父组件管理
- 值传递:通过
value属性传入当前状态 - 回调通知:通过
onChanged回调通知状态变化 - 禁用状态:
onChanged为null时组件禁用
// 父组件管理状态
bool _isEnabled = true;
Switch(
value: _isEnabled, // 当前状态
onChanged: (value) { // 状态变化回调
setState(() {
_isEnabled = value; // 更新状态
});
},
)
1️⃣ Switch(单选开关)
特点
- 用途:开启/关闭某个功能
- 状态:true(开)/ false(关)
- 外观:滑动开关样式
基本用法
class SwitchExample extends StatefulWidget {
@override
_SwitchExampleState createState() => _SwitchExampleState();
}
class _SwitchExampleState extends State<SwitchExample> {
bool _switchSelected = true;
@override
Widget build(BuildContext context) {
return Switch(
value: _switchSelected,
onChanged: (value) {
setState(() {
_switchSelected = value;
});
},
);
}
}
Switch 属性
| 属性 | 类型 | 说明 |
|---|---|---|
value | bool | 当前状态(必选) |
onChanged | ValueChanged? | 状态改变回调 |
activeColor | Color? | 激活状态颜色 |
activeTrackColor | Color? | 激活状态轨道颜色 |
inactiveThumbColor | Color? | 非激活状态滑块颜色 |
inactiveTrackColor | Color? | 非激活状态轨道颜色 |
自定义样式
Switch(
value: _switchSelected,
activeColor: Colors.red, // 激活时滑块颜色
activeTrackColor: Colors.red[100], // 激活时轨道颜色
inactiveThumbColor: Colors.grey, // 非激活时滑块颜色
inactiveTrackColor: Colors.grey[300], // 非激活时轨道颜色
onChanged: (value) {
setState(() {
_switchSelected = value;
});
},
)
2️⃣ Checkbox(复选框)
特点
- 用途:多选场景
- 状态:true(选中)/ false(未选中)/ null(不确定,三态模式)
- 外观:方形勾选框
基本用法
class CheckboxExample extends StatefulWidget {
@override
_CheckboxExampleState createState() => _CheckboxExampleState();
}
class _CheckboxExampleState extends State<CheckboxExample> {
bool _checkboxSelected = true;
@override
Widget build(BuildContext context) {
return Checkbox(
value: _checkboxSelected,
activeColor: Colors.red, // 选中时的颜色
onChanged: (value) {
setState(() {
_checkboxSelected = value!;
});
},
);
}
}
Checkbox 属性
| 属性 | 类型 | 说明 |
|---|---|---|
value | bool? | 当前状态(必选) |
onChanged | ValueChanged<bool?>? | 状态改变回调 |
activeColor | Color? | 选中状态颜色 |
checkColor | Color? | 勾选标记颜色 |
tristate | bool | 是否三态(默认false) |
三态复选框
bool? _checkbox = null; // null 表示不确定状态
Checkbox(
value: _checkbox,
tristate: true, // 启用三态
onChanged: (value) {
setState(() {
_checkbox = value;
// 循环:null -> false -> true -> null
});
},
)
三态说明:
false:未选中true:选中null:不确定(如"全选"按钮,部分子项选中时)
3️⃣ Radio(单选按钮)
特点
- 用途:一组选项中只能选择一个
- 状态:通过
groupValue确定哪个被选中 - 外观:圆形单选按钮
基本用法
class RadioExample extends StatefulWidget {
@override
_RadioExampleState createState() => _RadioExampleState();
}
class _RadioExampleState extends State<RadioExample> {
int _radioValue = 1;
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
Radio<int>(
value: 1,
groupValue: _radioValue,
onChanged: (value) {
setState(() {
_radioValue = value!;
});
},
),
Text('选项1'),
],
),
Row(
children: [
Radio<int>(
value: 2,
groupValue: _radioValue,
onChanged: (value) {
setState(() {
_radioValue = value!;
});
},
),
Text('选项2'),
],
),
Row(
children: [
Radio<int>(
value: 3,
groupValue: _radioValue,
onChanged: (value) {
setState(() {
_radioValue = value!;
});
},
),
Text('选项3'),
],
),
],
);
}
}
Radio 属性
| 属性 | 类型 | 说明 |
|---|---|---|
value | T | 该Radio代表的值 |
groupValue | T? | 当前选中的值 |
onChanged | ValueChanged<T?>? | 状态改变回调 |
activeColor | Color? | 选中状态颜色 |
工作原理
flowchart LR
A["Radio value=1<br/>groupValue=1"] --> B["选中"]
C["Radio value=2<br/>groupValue=1"] --> D["未选中"]
E["Radio value=3<br/>groupValue=1"] --> F["未选中"]
style A fill:#e1f5ff
style B fill:#e1ffe1
style C fill:#ffe1f5
style D fill:#ffe1e1
style E fill:#ffe1f5
style F fill:#ffe1e1
规则: 当 value == groupValue 时,该 Radio 被选中。
4️⃣ CheckboxListTile(带标题的复选框)
特点
- 集成了
Checkbox和ListTile - 支持标题、副标题、图标
- 整行可点击
基本用法
class CheckboxListTileExample extends StatefulWidget {
@override
_CheckboxListTileExampleState createState() => _CheckboxListTileExampleState();
}
class _CheckboxListTileExampleState extends State<CheckboxListTileExample> {
bool _item1 = true;
@override
Widget build(BuildContext context) {
return CheckboxListTile(
title: Text('接收通知'),
subtitle: Text('允许应用发送通知'),
value: _item1,
onChanged: (value) {
setState(() {
_item1 = value!;
});
},
secondary: Icon(Icons.notifications), // 左侧图标
);
}
}
CheckboxListTile 属性
| 属性 | 类型 | 说明 |
|---|---|---|
value | bool? | 当前状态 |
onChanged | ValueChanged<bool?>? | 状态改变回调 |
title | Widget? | 标题 |
subtitle | Widget? | 副标题 |
secondary | Widget? | 左侧图标 |
activeColor | Color? | 选中状态颜色 |
controlAffinity | ListTileControlAffinity | 控件位置 |
控件位置
// 复选框在右侧(默认)
CheckboxListTile(
title: Text('标题'),
value: true,
onChanged: (value) {},
controlAffinity: ListTileControlAffinity.trailing,
)
// 复选框在左侧
CheckboxListTile(
title: Text('标题'),
value: true,
onChanged: (value) {},
controlAffinity: ListTileControlAffinity.leading,
)
5️⃣ SwitchListTile(带标题的开关)
特点
- 集成了
Switch和ListTile - 用法与
CheckboxListTile类似
基本用法
class SwitchListTileExample extends StatefulWidget {
@override
_SwitchListTileExampleState createState() => _SwitchListTileExampleState();
}
class _SwitchListTileExampleState extends State<SwitchListTileExample> {
bool _wifi = true;
@override
Widget build(BuildContext context) {
return SwitchListTile(
title: Text('Wi-Fi'),
subtitle: Text('已连接到 Home'),
value: _wifi,
onChanged: (value) {
setState(() {
_wifi = value;
});
},
secondary: Icon(Icons.wifi),
activeColor: Colors.blue,
);
}
}
6️⃣ RadioListTile(带标题的单选按钮)
特点
- 集成了
Radio和ListTile - 用法与
CheckboxListTile类似
基本用法
class RadioListTileExample extends StatefulWidget {
@override
_RadioListTileExampleState createState() => _RadioListTileExampleState();
}
class _RadioListTileExampleState extends State<RadioListTileExample> {
String _theme = 'auto';
@override
Widget build(BuildContext context) {
return Column(
children: [
RadioListTile<String>(
title: Text('自动'),
subtitle: Text('跟随系统'),
value: 'auto',
groupValue: _theme,
onChanged: (value) {
setState(() {
_theme = value!;
});
},
secondary: Icon(Icons.brightness_auto),
),
RadioListTile<String>(
title: Text('浅色'),
subtitle: Text('始终使用浅色主题'),
value: 'light',
groupValue: _theme,
onChanged: (value) {
setState(() {
_theme = value!;
});
},
secondary: Icon(Icons.light_mode),
),
RadioListTile<String>(
title: Text('深色'),
subtitle: Text('始终使用深色主题'),
value: 'dark',
groupValue: _theme,
onChanged: (value) {
setState(() {
_theme = value!;
});
},
secondary: Icon(Icons.dark_mode),
),
],
);
}
}
💡 最佳实践
1. 状态管理模式
// ✅ 推荐:父组件管理状态
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _isEnabled = true;
@override
Widget build(BuildContext context) {
return Switch(
value: _isEnabled,
onChanged: (value) {
setState(() {
_isEnabled = value;
});
},
);
}
}
// ❌ 避免:Switch自己管理状态(Switch本身不支持)
2. 多选场景
使用 Set 或 List 管理多个选项:
class MultiSelectExample extends StatefulWidget {
@override
_MultiSelectExampleState createState() => _MultiSelectExampleState();
}
class _MultiSelectExampleState extends State<MultiSelectExample> {
final Set<String> _selectedItems = {};
final List<String> _items = ['选项1', '选项2', '选项3', '选项4'];
@override
Widget build(BuildContext context) {
return Column(
children: _items.map((item) {
return CheckboxListTile(
title: Text(item),
value: _selectedItems.contains(item),
onChanged: (checked) {
setState(() {
if (checked!) {
_selectedItems.add(item);
} else {
_selectedItems.remove(item);
}
});
},
);
}).toList(),
);
}
}
3. 全选功能
class SelectAllExample extends StatefulWidget {
@override
_SelectAllExampleState createState() => _SelectAllExampleState();
}
class _SelectAllExampleState extends State<SelectAllExample> {
final List<String> _items = ['选项1', '选项2', '选项3'];
final Set<String> _selectedItems = {};
bool? get _isAllSelected {
if (_selectedItems.isEmpty) return false;
if (_selectedItems.length == _items.length) return true;
return null; // 部分选中
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// 全选复选框(三态)
CheckboxListTile(
title: Text('全选'),
value: _isAllSelected,
tristate: true,
onChanged: (value) {
setState(() {
if (value == true) {
_selectedItems.addAll(_items);
} else {
_selectedItems.clear();
}
});
},
),
Divider(),
// 子项
..._items.map((item) {
return CheckboxListTile(
title: Text(item),
value: _selectedItems.contains(item),
onChanged: (checked) {
setState(() {
if (checked!) {
_selectedItems.add(item);
} else {
_selectedItems.remove(item);
}
});
},
);
}).toList(),
],
);
}
}
4. 禁用状态
// 禁用(onChanged 为 null)
Switch(
value: true,
onChanged: null, // 禁用
)
Checkbox(
value: true,
onChanged: null, // 禁用
)
Radio(
value: 1,
groupValue: 1,
onChanged: null, // 禁用
)
🤔 常见问题(FAQ)
Q1: 为什么组件不保存自己的状态?
A: 这是 Flutter 的设计哲学:
- 数据与UI分离:选择状态通常关联业务数据,应该由数据层管理
- 状态提升:方便父组件统一管理多个子组件的状态
- 灵活性:父组件可以决定何时、如何更新状态
// 场景:保存用户设置
class Settings {
bool notifications = true;
bool autoUpdate = false;
}
Settings _settings = Settings();
// Switch 的状态来自业务数据
Switch(
value: _settings.notifications,
onChanged: (value) {
setState(() {
_settings.notifications = value;
// 可以同时保存到数据库
saveSettings(_settings);
});
},
)
Q2: Checkbox 的 tristate 有什么用?
A: 三态用于表示"全选"状态:
true:全部选中false:全部未选中null:部分选中
// 示例:文件夹全选
// null = 部分文件选中
// true = 所有文件选中
// false = 没有文件选中
Q3: Radio 如何实现单选?
A: 通过 groupValue 实现:
int _selected = 1;
// 所有 Radio 共享同一个 groupValue
Radio(value: 1, groupValue: _selected, onChanged: ...),
Radio(value: 2, groupValue: _selected, onChanged: ...),
Radio(value: 3, groupValue: _selected, onChanged: ...),
// 当某个 Radio 被点击时,更新 _selected
// 只有 value == groupValue 的 Radio 会显示为选中状态
Q4: 如何自定义 Checkbox 和 Switch 的大小?
A: 目前无法直接修改大小,但可以使用 Transform.scale 缩放:
Transform.scale(
scale: 1.5, // 放大1.5倍
child: Checkbox(
value: true,
onChanged: (value) {},
),
)
Q5: CheckboxListTile 点击整行都会触发,如何只点击复选框才触发?
A: 不能禁用整行点击,但可以自己组合 ListTile 和 Checkbox:
ListTile(
title: Text('标题'),
trailing: Checkbox(
value: _value,
onChanged: (value) {
setState(() {
_value = value!;
});
},
),
// 不设置 onTap,只有点击 Checkbox 才会触发
)
🎯 跟着做练习
练习1:实现一个设置页面
目标: 创建一个设置页面,包含多个开关和复选框
步骤:
- 使用
SwitchListTile创建开关选项 - 使用
CheckboxListTile创建复选框选项 - 点击"保存"按钮显示当前设置
💡 查看答案
class SettingsPage extends StatefulWidget {
const SettingsPage({super.key});
@override
State<SettingsPage> createState() => _SettingsPageState();
}
class _SettingsPageState extends State<SettingsPage> {
bool _notifications = true;
bool _autoUpdate = false;
bool _saveData = true;
bool _vibration = true;
bool _sound = true;
void _showSettings() {
final settings = {
'通知': _notifications,
'自动更新': _autoUpdate,
'省流量模式': _saveData,
'振动': _vibration,
'声音': _sound,
};
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('当前设置'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: settings.entries.map((e) {
return Text('${e.key}: ${e.value ? "开启" : "关闭"}');
}).toList(),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('设置'),
centerTitle: true,
),
body: ListView(
children: [
SwitchListTile(
title: const Text('接收通知'),
subtitle: const Text('允许应用发送通知'),
value: _notifications,
onChanged: (value) => setState(() => _notifications = value),
secondary: const Icon(Icons.notifications),
),
SwitchListTile(
title: const Text('自动更新'),
subtitle: const Text('在WiFi环境下自动更新'),
value: _autoUpdate,
onChanged: (value) => setState(() => _autoUpdate = value),
secondary: const Icon(Icons.system_update),
),
CheckboxListTile(
title: const Text('省流量模式'),
subtitle: const Text('仅在WiFi下加载图片'),
value: _saveData,
onChanged: (value) => setState(() => _saveData = value!),
secondary: const Icon(Icons.data_saver_on),
),
CheckboxListTile(
title: const Text('振动'),
subtitle: const Text('操作时振动反馈'),
value: _vibration,
onChanged: (value) => setState(() => _vibration = value!),
secondary: const Icon(Icons.vibration),
),
CheckboxListTile(
title: const Text('声音'),
subtitle: const Text('操作时播放声音'),
value: _sound,
onChanged: (value) => setState(() => _sound = value!),
secondary: const Icon(Icons.volume_up),
),
Padding(
padding: const EdgeInsets.all(16),
child: ElevatedButton(
onPressed: _showSettings,
child: const Text('查看当前设置'),
),
),
],
),
);
}
}
练习2:实现一个问卷调查
目标: 创建一个问卷,包含单选和多选题
步骤:
- 使用
RadioListTile实现单选题 - 使用
CheckboxListTile实现多选题 - 收集答案并显示结果
💡 查看答案
class SurveyPage extends StatefulWidget {
const SurveyPage({super.key});
@override
State<SurveyPage> createState() => _SurveyPageState();
}
class _SurveyPageState extends State<SurveyPage> {
// 单选题答案
String? _gender;
String? _age;
// 多选题答案
final Set<String> _hobbies = {};
void _submitSurvey() {
if (_gender == null || _age == null || _hobbies.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('请完成所有问题')),
);
return;
}
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('问卷结果'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('性别:$_gender'),
Text('年龄段:$_age'),
Text('兴趣爱好:${_hobbies.join('、')}'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('确定'),
),
],
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('问卷调查'),
centerTitle: true,
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
const Text(
'1. 您的性别?',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
RadioListTile<String>(
title: const Text('男'),
value: '男',
groupValue: _gender,
onChanged: (value) => setState(() => _gender = value),
),
RadioListTile<String>(
title: const Text('女'),
value: '女',
groupValue: _gender,
onChanged: (value) => setState(() => _gender = value),
),
const Divider(height: 32),
const Text(
'2. 您的年龄段?',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
RadioListTile<String>(
title: const Text('18岁以下'),
value: '18岁以下',
groupValue: _age,
onChanged: (value) => setState(() => _age = value),
),
RadioListTile<String>(
title: const Text('18-30岁'),
value: '18-30岁',
groupValue: _age,
onChanged: (value) => setState(() => _age = value),
),
RadioListTile<String>(
title: const Text('31-50岁'),
value: '31-50岁',
groupValue: _age,
onChanged: (value) => setState(() => _age = value),
),
RadioListTile<String>(
title: const Text('50岁以上'),
value: '50岁以上',
groupValue: _age,
onChanged: (value) => setState(() => _age = value),
),
const Divider(height: 32),
const Text(
'3. 您的兴趣爱好?(多选)',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
CheckboxListTile(
title: const Text('阅读'),
value: _hobbies.contains('阅读'),
onChanged: (checked) {
setState(() {
if (checked!) {
_hobbies.add('阅读');
} else {
_hobbies.remove('阅读');
}
});
},
),
CheckboxListTile(
title: const Text('运动'),
value: _hobbies.contains('运动'),
onChanged: (checked) {
setState(() {
if (checked!) {
_hobbies.add('运动');
} else {
_hobbies.remove('运动');
}
});
},
),
CheckboxListTile(
title: const Text('音乐'),
value: _hobbies.contains('音乐'),
onChanged: (checked) {
setState(() {
if (checked!) {
_hobbies.add('音乐');
} else {
_hobbies.remove('音乐');
}
});
},
),
CheckboxListTile(
title: const Text('旅行'),
value: _hobbies.contains('旅行'),
onChanged: (checked) {
setState(() {
if (checked!) {
_hobbies.add('旅行');
} else {
_hobbies.remove('旅行');
}
});
},
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _submitSurvey,
child: const Text('提交问卷'),
),
],
),
);
}
}
📋 小结
核心要点
| 组件 | 用途 | 状态类型 |
|---|---|---|
| Switch | 开关功能 | bool |
| Checkbox | 多选 | bool / bool? (三态) |
| Radio | 单选 | T (泛型) |
ListTile 版本对比
| 组件 | 优势 | 适用场景 |
|---|---|---|
| 基础版 | 灵活、可自定义布局 | 简单场景 |
| ListTile版 | 自带标题、副标题、图标 | 设置页面、表单 |
状态管理原则
- 父组件管理:选择组件的状态由父组件维护
- 单向数据流:父 → 子(value),子 → 父(onChanged)
- 及时更新:onChanged 中调用
setState更新UI