第3章:基础组件 —— 3.4 单选开关和复选框

59 阅读9分钟

3.4 单选开关和复选框

📚 章节概览

选择组件是用户界面中常见的交互元素。Flutter 提供了 Material 风格的选择组件,本章节将学习:

  • Switch - 单选开关(开/关)
  • Checkbox - 复选框(多选)
  • Radio - 单选按钮(单选)
  • CheckboxListTile - 带标题的复选框
  • SwitchListTile - 带标题的开关
  • RadioListTile - 带标题的单选按钮

🎯 核心知识点

重要特性

所有选择组件都有以下共同特点:

  1. 状态管理:组件本身不保存状态,状态由父组件管理
  2. 值传递:通过 value 属性传入当前状态
  3. 回调通知:通过 onChanged 回调通知状态变化
  4. 禁用状态onChangednull 时组件禁用
// 父组件管理状态
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 属性

属性类型说明
valuebool当前状态(必选)
onChangedValueChanged?状态改变回调
activeColorColor?激活状态颜色
activeTrackColorColor?激活状态轨道颜色
inactiveThumbColorColor?非激活状态滑块颜色
inactiveTrackColorColor?非激活状态轨道颜色

自定义样式

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 属性

属性类型说明
valuebool?当前状态(必选)
onChangedValueChanged<bool?>?状态改变回调
activeColorColor?选中状态颜色
checkColorColor?勾选标记颜色
tristatebool是否三态(默认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 属性

属性类型说明
valueT该Radio代表的值
groupValueT?当前选中的值
onChangedValueChanged<T?>?状态改变回调
activeColorColor?选中状态颜色

工作原理

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(带标题的复选框)

特点

  • 集成了 CheckboxListTile
  • 支持标题、副标题、图标
  • 整行可点击

基本用法

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 属性

属性类型说明
valuebool?当前状态
onChangedValueChanged<bool?>?状态改变回调
titleWidget?标题
subtitleWidget?副标题
secondaryWidget?左侧图标
activeColorColor?选中状态颜色
controlAffinityListTileControlAffinity控件位置

控件位置

// 复选框在右侧(默认)
CheckboxListTile(
  title: Text('标题'),
  value: true,
  onChanged: (value) {},
  controlAffinity: ListTileControlAffinity.trailing,
)

// 复选框在左侧
CheckboxListTile(
  title: Text('标题'),
  value: true,
  onChanged: (value) {},
  controlAffinity: ListTileControlAffinity.leading,
)

5️⃣ SwitchListTile(带标题的开关)

特点

  • 集成了 SwitchListTile
  • 用法与 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(带标题的单选按钮)

特点

  • 集成了 RadioListTile
  • 用法与 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. 多选场景

使用 SetList 管理多个选项:

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 的设计哲学:

  1. 数据与UI分离:选择状态通常关联业务数据,应该由数据层管理
  2. 状态提升:方便父组件统一管理多个子组件的状态
  3. 灵活性:父组件可以决定何时、如何更新状态
// 场景:保存用户设置
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: 不能禁用整行点击,但可以自己组合 ListTileCheckbox

ListTile(
  title: Text('标题'),
  trailing: Checkbox(
    value: _value,
    onChanged: (value) {
      setState(() {
        _value = value!;
      });
    },
  ),
  // 不设置 onTap,只有点击 Checkbox 才会触发
)

🎯 跟着做练习

练习1:实现一个设置页面

目标: 创建一个设置页面,包含多个开关和复选框

步骤:

  1. 使用 SwitchListTile 创建开关选项
  2. 使用 CheckboxListTile 创建复选框选项
  3. 点击"保存"按钮显示当前设置
💡 查看答案
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:实现一个问卷调查

目标: 创建一个问卷,包含单选和多选题

步骤:

  1. 使用 RadioListTile 实现单选题
  2. 使用 CheckboxListTile 实现多选题
  3. 收集答案并显示结果
💡 查看答案
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版自带标题、副标题、图标设置页面、表单

状态管理原则

  1. 父组件管理:选择组件的状态由父组件维护
  2. 单向数据流:父 → 子(value),子 → 父(onChanged)
  3. 及时更新:onChanged 中调用 setState 更新UI

🔗 相关资源