在做鸿蒙上的Flutter设置页时,单选按钮(Radio)是绕不开的基础组件。ArkTS中有Radio+RadioGroup,Flutter中也提供Radio和RadioListTile。实际开发中发现,Flutter的Radio组件在鸿蒙上虽然兼容性没问题,但细节处理与ArkTS不同,需要特别注意。
本文基于真实项目,用五个场景拆解Radio的使用,涵盖基础性别选择、颜色选择、支付方式卡片、无默认值尺寸选择和评分组件。所有代码均在nova 12 Ultra(鸿蒙6.0)和Flutter 3.22上验证通过。
先统一展示五种状态变量的声明:- String _gender = 'male'; // 场景一:性别
- String _color = 'blue'; // 场景二:颜色
- String _payment = 'alipay'; // 场景三:支付方式
- String? _size; // 场景四:尺寸(无默认值)
- int? _rating = 3; // 场景五:评分
复制代码
Flutter的Radio是泛型组件<T>,三个必传参数:value、groupValue、onChanged。选中逻辑:value==groupValue时显示为选中。与Checkbox不同,Radio没有半选状态。RadioListTile是带文字和布局的版本,支持title、subtitle、secondary(左侧图标)、activeColor等属性,点击区域覆盖整个tile。
ArkTS用RadioGroup容器管理组,子Radio声明value,父子关系天然形成互斥组。Flutter则更扁平——通过将多个Radio共享同一个groupValue变量建立关联,没有显式容器。两种设计各有优劣:ArkTS声明式更直观,Flutter泛型约束更安全(编译期检查类型)。
场景一:基础Radio自定义样式——性别选择
默认Radio仅一个小圆点,太素。我习惯自定义成卡片样式,带图标和文字,选中时加蓝色边框。用GestureDetector包Container,根据选中状态切换背景色和边框。实际并未使用Radio组件,而是模拟单选行为。- Widget _buildRadio(String value, IconData icon, String label) {
- final isSelected = _gender == value;
- return GestureDetector(
- onTap: () => setState(() => _gender = value),
- child: Container(
- padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
- decoration: BoxDecoration(
- color: isSelected ? Colors.blue.shade50 : Colors.grey.shade50,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: isSelected ? Colors.blue : Colors.grey.shade300,
- width: isSelected ? 2 : 1,
- ),
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Icon(icon, color: isSelected ? Colors.blue : Colors.grey),
- const SizedBox(width: 6),
- Text(label),
- ],
- ),
- ),
- );
- }
复制代码 调用时一行一个。这种自定义方式样式可控,但失去标准Radio的无障碍语义,需要时可外包Semantics或改用InkWell加涟漪效果。
场景二:RadioListTile颜色选择
RadioListTile适合设置页,左侧显示色块图标。- ...['blue', 'green', 'orange', 'purple'].map((c) {
- return RadioListTile<String>(
- title: Text(c),
- subtitle: Text('选择 $c 色'),
- secondary: Icon(Icons.palette, color: _parseColor(c)),
- value: c,
- groupValue: _color,
- onChanged: (v) => setState(() => _color = v!),
- );
- })
复制代码 注意groupValue类型必须与value一致,编译期检查。鸿蒙上Material Design的涟漪反馈效果正常,体验接近原生。
场景三:自定义卡片Radio——支付方式选择
电商App常见。将支付方式做成卡片,选中时边框变色,Radio圆点颜色跟随。使用Card+RadioListTile组合。- Widget _buildPaymentRadio(
- String value, String label, IconData icon, MaterialColor color) {
- final isSelected = _payment == value;
- return Card(
- margin: const EdgeInsets.only(bottom: 8),
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(12),
- side: BorderSide(
- color: isSelected ? color : Colors.grey.shade300,
- width: isSelected ? 2 : 1,
- ),
- ),
- child: RadioListTile<String>(
- value: value,
- groupValue: _payment,
- onChanged: (v) => setState(() => _payment = v!),
- title: Text(label),
- secondary: Icon(icon, color: isSelected ? color : Colors.grey),
- activeColor: color,
- ),
- );
- }
复制代码 activeColor控制圆点颜色。Card边框通过shape.side设置,但阴影和边框是两套系统。保持统一elevation,用边框颜色区分选中态更稳妥。
场景四:尺寸选择——无默认值 + 自定义样式
某些场景需要用户主动选择,初始无默认值。如S/M/L/XL尺寸。- String? _size;
- Widget _buildSizeRadio(String size) {
- final isSelected = _size == size;
- return GestureDetector(
- onTap: () => setState(() => _size = size),
- child: Container(
- width: 50, height: 50,
- decoration: BoxDecoration(
- color: isSelected ? Colors.teal.shade50 : Colors.grey.shade50,
- borderRadius: BorderRadius.circular(12),
- border: Border.all(
- color: isSelected ? Colors.teal : Colors.grey.shade300,
- width: isSelected ? 2 : 1,
- ),
- ),
- child: Center(child: Text(size, style: TextStyle(...))),
- ),
- );
- }
复制代码 UI上通过_size==null显示“请选择尺寸”。注意groupValue也得是String?,否则编译报错。也可以采用空字符串默认值,避免处理nullable。
场景五:评分——Wrap+Radio模拟
评分用星星图标表示程度,而非选项。使用InkWell+Icon模拟,用Wrap布局适应换行。- Wrap(
- spacing: 4,
- children: List.generate(5, (i) {
- final star = i + 1;
- return InkWell(
- onTap: () => setState(() => _rating = star),
- child: Padding(
- padding: const EdgeInsets.all(4),
- child: Icon(
- star <= _rating! ? Icons.star : Icons.star_border,
- color: Colors.amber, size: 36,
- ),
- ),
- );
- }),
- )
复制代码 Wrap的spacing控制主轴间距,runSpacing控制交叉轴间距,若换行必须设置。
Radio与Checkbox的选择区分:互斥用Radio,多选用Checkbox。不同场景也可考虑ChoiceChip,更紧凑。
开发中踩过的坑总结:
1. groupValue类型必须严格一致,泛型编译期检查。
2. 裸Radio只有圆点,必须包文字或使用RadioListTile。
3. 默认Radio不支持取消选中,若需要可设toggleable:true,但onChanged会返回null。
4. RadioListTile布局固定,要自定义需手写GestureDetector+Container。
5. onChanged回调参数可空,需解包(v!),但要注意groupValue非null时不会返回null。
6. Wrap设置间距时不要遗漏runSpacing。
7. RadioListTile默认高度56dp,视觉密度紧凑可用visualDensity: VisualDensity.compact。
8. fillColor通过MaterialStateProperty可分别控制选中/未选中颜色。
9. RadioListTile的contentPadding默认左右16dp,调整需手动设置。
在鸿蒙上运行原生的Flutter Material组件,Radio的点击反馈、水波纹动画完全正常,兼容性没问题。但注意鸿蒙上Material 3主题渲染颜色饱和度略低于Android(如activeColor在鸿蒙上偏淡),这是平台差异,代码逻辑无需修改。
以上五个场景覆盖了Radio的主要使用模式:用标准Radio组件(RadioListTile)或模拟Radio(GestureDetector/InkWell)。前者适合列表设置页,后者适合需要视觉差异化或自定义布局的场景。在鸿蒙Flutter开发中,用好Radio组件能显著提升设置页的用户体验。 |