在鸿蒙应用开发中,Flutter 是不少团队选择的跨端方案,而设置页里最常见的交互组件就是开关。无论是 Wi-Fi、蓝牙、通知,还是深色模式,几乎每个设置页都离不开 Switch。ArkTS 里对应的是 Toggle,Flutter 里是 Switch,两者用法高度相似。本文结合鸿蒙 Flutter 实际开发场景,梳理 Switch 从基础用法到设置页实战的完整路径,重点讲清楚 SwitchListTile、自定义颜色、禁用态、联动控制、数据持久化、深色模式适配,以及踩过的坑。
["color=#3C3C3C"]基础 Switch:一个 bool 一个 setState
Flutter 里 Switch 最简用法就是绑定一个 bool 变量,在 onChanged 回调里 setState 刷新 UI。和按钮组件不同,Switch 的状态变化完全靠 onChanged 驱动,没有 onTap 或 onPressed 的概念。
- bool _wifi = true;
- Switch(
- value: _wifi,
- onChanged: (v) => setState(() => _wifi = v),
- )
复制代码
value 是当前开关状态,onChanged 在用户操作时回调新值。这里要注意,onChanged 里的逻辑应该尽量轻量,只做状态更新,不要放耗时操作。
["color=#3C3C3C"]SwitchListTile:设置页标配布局
设置页里每条设置项基本是“左侧文字图标 + 右侧开关”的结构。如果手写 Row + Text + Switch,代码会很长,而且对齐、间距都要自己算。Flutter 提供的 SwitchListTile 一行就能解决。
- SwitchListTile(
- title: const Text('推送通知'),
- subtitle: const Text('开启后接收活动通知和消息'),
- secondary: const Icon(Icons.notifications_outlined),
- value: _notify,
- onChanged: (v) => setState(() => _notify = v),
- dense: true,
- )
复制代码
title 是主标题,subtitle 是副标题(灰色小字),secondary 是左侧图标。dense: true 会让内边距更紧凑,适合设置页。默认高度 72dp,加 dense 后是 56dp。多个设置项连续排列时,用 Column 配合 Divider 分段即可。
- Column(
- children: [
- SwitchListTile(title: Text('Wi-Fi'), value: _wifi, onChanged: (v) {}),
- Divider(height: 1),
- SwitchListTile(title: Text('蓝牙'), value: _bluetooth, onChanged: (v) {}),
- ],
- )
复制代码
["color=#3C3C3C"]自定义颜色:activeColor 与 activeTrackColor 要一起设
Switch 的滑块和轨道颜色都可以单独控制。
- Switch(
- value: _bluetooth,
- activeColor: Colors.blue, // 开启时滑块颜色
- activeTrackColor: Colors.blue.shade100, // 开启时轨道颜色
- inactiveThumbColor: Colors.grey, // 关闭时滑块颜色
- inactiveTrackColor: Colors.grey.shade200, // 关闭时轨道颜色
- onChanged: (v) => setState(() => _bluetooth = v),
- )
复制代码
常见问题是只设 activeColor,轨道颜色会自动适应,但透明度跟默认状态不一致,看起来不够协调。建议 activeColor 和 activeTrackColor 一起设置。SwitchListTile 没有直接暴露这些颜色参数,需要通过 Theme 组件统一覆盖。
["color=#3C3C3C"]禁用状态:onChanged 设为 null
Switch 的禁用很简单,把 onChanged 设为 null,组件自动变灰且不可操作。
- Switch(
- value: true,
- onChanged: null, // 禁用
- )
复制代码
如果希望禁用时保持某种颜色(不自动变灰),可以用 Theme 覆盖 disabledColor。但这里要提醒一句:禁用状态用灰色是 Material Design 的平台一致性要求,不建议在常规场景下覆盖。如果确实需要,也要只在特殊场景下用。
["color=#3C3C3C"]总开关联动子开关:IgnorePointer + Opacity 组合
设置页里“消息通知”总开关控制“声音”“振动”等子开关,是非常典型的模式。直接给子开关设 onChanged: null 有一个问题:禁用时子开关的选中状态看不出来,重新启用后状态也容易丢失。更好的做法是外层包 IgnorePointer 拦截触摸事件,再用 Opacity 降低透明度表示禁用。
- bool _masterOn = true;
- bool _soundOn = true;
- bool _vibrateOn = false;
- // 总开关
- SwitchListTile(
- title: Text('消息通知'),
- value: _masterOn,
- onChanged: (v) {
- setState(() {
- _masterOn = v;
- // 关总开关时,同时关掉子开关
- if (!v) { _soundOn = false; _vibrateOn = false; }
- });
- },
- )
- // 子开关
- IgnorePointer(
- ignoring: !_masterOn,
- child: Opacity(
- opacity: _masterOn ? 1.0 : 0.4,
- child: SwitchListTile(
- title: Text('声音'),
- value: _soundOn,
- onChanged: (v) => setState(() => _soundOn = v),
- ),
- ),
- )
复制代码
这种方式的优势在于:子开关的选中状态不会因为禁用而丢失。总开关重新打开时,子开关能恢复到之前用户设置的偏好,体验上比直接置灰更细腻。
["color=#3C3C3C"]Switch + Slider 联动:音量设置的常见形态
有些设置项同时需要开关和数值调节,比如音量。Switch 控制开启/关闭,Slider 控制具体数值。系统设置页里很常见的交互是:关掉声音开关时,音量滑块自动置零且不可拖拽。
- bool _volumeOn = true;
- double _volume = 0.5;
- Row(
- children: [
- Icon(Icons.volume_up),
- Text('音量'),
- Spacer(),
- Switch(
- value: _volumeOn,
- onChanged: (v) => setState(() {
- _volumeOn = v;
- if (!v) _volume = 0;
- }),
- ),
- ],
- )
- Opacity(
- opacity: _volumeOn ? 1.0 : 0.4,
- child: AbsorbPointer(
- absorbing: !_volumeOn,
- child: Slider(
- value: _volume,
- onChanged: (v) => setState(() => _volume = v),
- ),
- ),
- )
复制代码
这里用了 AbsorbPointer 而不是 IgnorePointer,两者区别在于:AbsorbPointer 会拦截事件但不影响自身布局,IgnorePointer 则完全忽略事件。对于 Slider 这类需要视觉反馈的组件,AbsorbPointer 更合适。
["color=#3C3C3C"]与 ArkTS Toggle 的对比
从鸿蒙 ArkTS 切到 Flutter 的开发者会发现,Flutter 的 Switch 比 ArkTS 的 Toggle 少了 ToggleType.Checkbox 模式——ArkTS 的 Toggle 可以当复选框用。但 Flutter 有专门的 Checkbox 组件,所以这种差异不算缺失,只是组件划分方式不同。在实际项目里,Flutter 的职责划分更清晰,Switch 管开关,Checkbox 管多选。
["color=#3C3C3C"]踩过的坑
第一个坑是 Switch 尺寸无法缩放。Switch 的宽高是固定值,SizedBox 包不住它,始终按 Material Design 规范尺寸渲染。如果确实需要特别大或特别小的开关,只能用 Transform.scale 做视觉缩放,但注意点击区域不会同步缩放,需要额外处理。
第二个坑与缩放相关:如果父级包了 Transform.scale 或 RotatedBox,Switch 的点击区域可能跟视觉位置不匹配。解决方案是把 Switch 放在最内层,缩放放在外层。
第三个坑是 SwitchListTile 的 contentPadding。默认有左右内边距,如果要做列表左对齐或跟边缘对齐,需要显式设置 contentPadding: EdgeInsets.zero。
第四个坑是 iOS 风格问题。如果 ThemeData.platform 被设为 TargetPlatform.iOS,Switch 会渲染成 iOS 风格的绿色开关。鸿蒙应用里如果希望各端风格统一,不要设置 platform 为 iOS。
最后一个坑是 onChanged 里的耗时操作。onChanged 回调在 UI 线程执行,如果在这里做网络请求或数据库写入,会导致界面卡顿。正确做法是在 onChanged 里只更新状态,耗时操作放到异步方法里。
["color=#3C3C3C"]写在最后
Switch 虽然是个很小的组件,但设置页几乎每个页面都要用到。SwitchListTile 省掉了大量手写布局代码,一行就能完成“左侧文字图标 + 右侧开关”的标准设置项。实际开发中,建议多利用 IgnorePointer + Opacity / AbsorbPointer + Opacity 组合实现可控禁用态,比直接设 onChanged: null 更灵活,用户状态保留也更完整。
本文的实践基于鸿蒙平台 Flutter 开发环境,Switch 组件的这些特性在 OpenHarmony 和 HarmonyOS 的 Flutter 工程中都适用。如果同时做 ArkTS 和 Flutter 两个版本,可以对比 Toggle 和 Switch 的差异,把同一套交互逻辑在两套代码库中各自实现到位。 |