
一、引言为什么需要单选组件1.1 单选 vs 多选在用户界面中当需要让用户从多个互斥的选项中做出唯一选择时单选框Radio是最直观的交互方式。它与复选框Checkbox的核心区别在于特性 单选框 (Radio) 复选框 (Checkbox)选择数量 只能选一个 可以选多个互斥关系 同组自动互斥 选项独立适用场景 性别、支付方式、尺寸选择 兴趣爱好、多选标签视觉样式 圆形选中点 方形勾选标记1.2 鸿蒙 Radio 组件概述鸿蒙 ArkUI 的 Radio 组件是一个轻量级的单选框使用时需要注意以下几点每个 Radio 必须属于一个分组通过 group 属性指定组名同组 Radio 自动互斥每个 Radio 必须有唯一标识通过 value 属性定义用于区分不同的选项选中状态由数据驱动通过 .checked() 方法绑定布尔值状态变化通过回调通知通过 .onChange() 监听选中/取消事件核心 API 签名如下Radio(options?: { value?: string, group?: string, indicator?: IndicatorStyle })属性 类型 说明value string 选项的唯一标识值group string 所属分组名称同组互斥indicator IndicatorStyle 指示器样式可选可自定义颜色二、项目结构总览演示项目包含一个核心页面文件和对应的页面路由entry/src/main/ets/pages/RadioDemo.ets ← 主演示页面480 行entry/src/main/resources/base/profile/└── main_pages.json ← 页面路由注册页面在首页索引中编号为 03 Radio 单选框使用 router.pushUrl({ url: ‘pages/RadioDemo’ }) 跳转。2.1 页面布局架构Column全屏safeAreaPadding 避让状态栏/导航栏├── buildHeader() 标题区└── Scroll内容可滚动├── buildThemeGroup() 分组一横向主题选择├── buildDivider() 分隔线├── buildPayGroup() 分组二纵向支付方式├── buildDivider() 分隔线├── buildSizeGroup() 分组三卡片式尺寸选择├── buildDivider() 分隔线└── buildResultSummary() 选择结果汇总 提交按钮2.2 三种演示布局对比分组 布局方式 数据条数 选中反馈 适用场景 主题颜色 Row 横向排列 3 色块边框高亮 文字加粗 主题/配色选择 支付方式 Column 纵向排列 3 整行边框变色 Radio 勾选 设置页/订单页☕ 尺寸选择 Row 卡片式排列 3 卡片背景变色 文字变色 商品规格/套餐三、核心 API 详解3.1 Radio 组件的构造函数Radio({ value: ‘blue’, group: ‘themeGroup’ })参数说明value该 Radio 的值在 onChange 回调中通过此值区分是哪个选项被选中。group分组标识。相同 group 的 Radio 形成单选组选中其中一个会自动取消其他同组 Radio 的选中状态。indicator可选指示器样式。可以设置 IndicatorStyle 自定义圆点的颜色和样式。3.2 checked() — 选中状态绑定.checked(this.selectedTheme ‘blue’).checked() 接收一个布尔值true 表示该 Radio 被选中false 表示未选中。因为是数据驱动的模式你需要维护一个 State 变量来存储当前选中的值然后与每个 Radio 的 value 做比较。3.3 onChange() — 状态变化回调.onChange((isChecked: boolean) {if (isChecked) {this.selectedTheme ‘blue’;}})onChange 回调的参数 isChecked 表示该 Radio 是否被选中。当用户点击一个未选中的 Radio 时该 Radio 的 isChecked 变为 true同组其他已选中的 Radio 的 isChecked 变为 false因此你只需要在 isChecked true 时更新对应的 State 变量即可。3.4 关于 RadioGroup 的说明在较早版本的 HarmonyOS 文档中存在 RadioGroup 容器组件它可以包裹一组 Radio 并统一管理 onChange 回调。但在 HarmonyOS NEXT API 12 的某些版本中RadioGroup 并未提供。本示例采用更通用的方案——直接在每个 Radio 上通过 group 字符串属性进行分组这种方式在所有版本中均兼容。四、代码深度解析4.1 数据类型定义// ── 主题选项 ──interface ThemeItem {value: string; // Radio 的 valuelabel: string; // 显示文字color: ResourceColor; // 主题色}// ── 支付选项 ──interface PayItem {value: string; // Radio 的 valuelabel: string; // 标题desc: string; // 描述文字icon: string; // Emoji 图标}// ── 尺寸选项 ──interface SizeItem {value: string;label: string;price: string; // 价格}三个接口分别对应三种演示布局。注意每个接口都包含 value: string这是与 Radio 组件绑定的关键字段。4.2 Entry 主组件与状态管理EntryComponentstruct RadioDemo {State private selectedTheme: string ‘blue’;State private selectedPay: string ‘wechat’;State private selectedSize: string ‘medium’;private readonly themeList: ThemeItem[] [{ value: ‘blue’, label: ‘天蓝’, color: ‘#317AF7’ },{ value: ‘green’, label: ‘翠绿’, color: ‘#00B578’ },{ value: ‘orange’, label: ‘暖橙’, color: ‘#FF7B2C’ }];// … payList, sizeList 类似}设计要点三个 State 变量分别管理三组 Radio 的选中值每组数据都包含一个与 Radio value 对应的字段确保数据驱动的一致性初始默认值分别为 ‘blue’、‘wechat’、‘medium’页面加载时第一项即处于选中状态4.3 根布局与可滚动容器build() {Column() {this.buildHeader()Scroll() {Column() {this.buildThemeGroup()this.buildDivider()this.buildPayGroup()this.buildDivider()this.buildSizeGroup()this.buildDivider()this.buildResultSummary()}.width(‘100%’).padding({ bottom: 24 })}.layoutWeight(1).width(‘100%’)}.width(‘100%’).height(‘100%’).backgroundColor(‘#F5F5F5’).safeAreaPadding({ top: SafeAreaType.SYSTEM, bottom: SafeAreaType.SYSTEM })}使用 Scroll 包裹内容区域是因为三个演示组 结果汇总区的总高度可能超出屏幕。.layoutWeight(1) 让 Scroll 填充剩余空间。最外层 Column 应用 safeAreaPadding 避让系统 UI。4.4 分组一横向主题选择BuilderbuildThemeGroup() {Column() {Text(‘ 主题颜色横向 Radio 布局’).fontSize(16).fontWeight(FontWeight.Bold).margin({ left: 20, top: 8, bottom: 12 })Row({ space: 12 }) { ForEach(this.themeList, (item: ThemeItem) { Column({ space: 6 }) { // 色块指示器 Row().width(32).height(32).borderRadius(16) .backgroundColor(item.color) .border({ color: this.selectedTheme item.value ? #1A1A1A : transparent, width: 2 }) // Radio 组件 Radio({ value: item.value, group: themeGroup }) .checked(this.selectedTheme item.value) .height(24) .onChange((isChecked: boolean) { if (isChecked) { this.selectedTheme item.value; } }) // 文字标签 Text(item.label) .fontSize(13) .fontColor(this.selectedTheme item.value ? item.color : #666666) .fontWeight(this.selectedTheme item.value ? FontWeight.Bold : FontWeight.Normal) } .alignItems(HorizontalAlign.Center) .layoutWeight(1) }) } .width(100%).padding({ left: 20, right: 20 }) Text(已选: ${this.getThemeLabel(this.selectedTheme)}) .fontSize(13).fontColor(#317AF7).fontWeight(FontWeight.Medium) .margin({ left: 20, top: 8 })}.width(‘100%’).padding({ top: 8, bottom: 8 })}布局要点Row 横向排列Row({ space: 12 }) 让三个 Radio 水平均分空间layoutWeight(1) 确保每个选项宽度相等。色块 Radio 文字三层结构每项使用 Column({ space: 6 }) 垂直排列三个子元素。选中视觉反馈色块在选中时添加黑色外圈边框border.color 切换文字在选中时变成主题色并加粗Radio 交互.checked() 绑定 selectedTheme 比较结果.onChange() 在选中时更新 selectedTheme。4.5 分组二纵向支付方式BuilderbuildPayGroup() {Column() {Text(‘ 支付方式纵向 Radio 布局’).fontSize(16).fontWeight(FontWeight.Bold).margin({ left: 20, top: 8, bottom: 12 })Column({ space: 10 }) { ForEach(this.payList, (item: PayItem) { Row() { Text(item.icon).fontSize(28).margin({ left: 16, right: 12 }) Column({ space: 2 }) { Text(item.label).fontSize(16).fontColor(#1A1A1A).fontWeight(FontWeight.Medium) Text(item.desc).fontSize(12).fontColor(#999999) } .layoutWeight(1).alignItems(HorizontalAlign.Start) Radio({ value: item.value, group: payGroup }) .checked(this.selectedPay item.value) .margin({ right: 16 }) .onChange((isChecked: boolean) { if (isChecked) { this.selectedPay item.value; } }) } .width(90%).height(64) .backgroundColor(#FFFFFF).borderRadius(14) .border({ color: this.selectedPay item.value ? #317AF7 : #E8E8E8, width: this.selectedPay item.value ? 2 : 1 }) .onClick(() { this.selectedPay item.value; }) }) } .width(100%).alignItems(HorizontalAlign.Center) Text(已选: ${this.getPayLabel(this.selectedPay)}) .fontSize(13).fontColor(#317AF7).fontWeight(FontWeight.Medium) .margin({ left: 20, top: 8 })}.width(‘100%’).padding({ top: 8, bottom: 8 })}布局要点Column 纵向排列Column({ space: 10 }) 垂直排列三个支付选项块。整行可点击通过 Row 的 .onClick() 直接更新 selectedPay。这是一种增强用户体验的技巧——用户点击整行任意位置都能选中而不必精确点击 Radio 圆点。视觉反馈选中时边框颜色变为蓝色#317AF7且宽度从 1px 变为 2pxRadio 圆点同步勾选数据同步Row.onClick 和 Radio.onChange 都更新同一个 State 变量两者触发效果一致。为什么既用 Click 又用 onChangeRow.onClick 提升点击热区让整行可点Radio.onChange 确保直接点击 Radio 圆点时也能更新状态两者同时存在时用户点击 Radio 区域会同时触发两个回调但由于 State 幂等更新不会产生冲突4.6 分组三卡片式尺寸选择BuilderbuildSizeGroup() {Column() {Text(‘☕ 选择尺寸卡片式 Radio 布局’).fontSize(16).fontWeight(FontWeight.Bold).margin({ left: 20, top: 8, bottom: 12 })Row({ space: 12 }) { ForEach(this.sizeList, (item: SizeItem) { Stack() { // 背景卡片 Row().width(100%).height(100%).borderRadius(16) .backgroundColor(this.selectedSize item.value ? #E8F0FE : #FFFFFF) .border({ color: this.selectedSize item.value ? #317AF7 : #E0E0E0, width: this.selectedSize item.value ? 2 : 1 }) // 内容 Column({ space: 6 }) { Text(item.label).fontSize(17).fontWeight(FontWeight.Bold) .fontColor(this.selectedSize item.value ? #317AF7 : #1A1A1A) Text(item.price).fontSize(20) .fontColor(this.selectedSize item.value ? #317AF7 : #FF7B2C) .fontWeight(FontWeight.Bold) // Radio 透明隐藏保留逻辑 Radio({ value: item.value, group: sizeGroup }) .checked(this.selectedSize item.value) .width(1).height(1).opacity(0) .onChange((isChecked: boolean) { if (isChecked) { this.selectedSize item.value; } }) } .alignItems(HorizontalAlign.Center).width(100%) } .width(28%).aspectRatio(1).layoutWeight(1) .onClick(() { this.selectedSize item.value; }) }) } .width(90%).padding({ left: 16, right: 16 }) Text(已选: ${this.getSizeLabel(this.selectedSize)}) .fontSize(13).fontColor(#317AF7).fontWeight(FontWeight.Medium) .margin({ left: 20, top: 8 })}.width(‘100%’).padding({ top: 8, bottom: 8 })}布局要点卡片式容器每个选项使用 Stack 包裹背景用 Row 实现圆角卡片内容用 Column 居中。Radio 透明隐藏Radio 通过 .width(1).height(1).opacity(0) 在视觉上隐藏但保留 group 和 value 逻辑——这样 Radio 的互斥逻辑仍然生效但界面完全由自定义卡片控制。选中视觉反馈背景色从白色变为蓝色淡色#E8F0FE边框从灰色变为蓝色宽度增加文字颜色从深色变为蓝色aspectRatio(1)保持卡片为正方形比例layoutWeight(1) 使三张卡片等宽。五、数据流分析5.1 单选组的数据流用户点击 Radio│▼Radio.onChange(isCheckedtrue)│▼更新 State selectedTheme ‘blue’│▼ArkUI 框架重新渲染│▼Radio({ value: ‘blue’ }).checked(true) ← 被选中Radio({ value: ‘green’ }).checked(false) ← 同组自动取消Radio({ value: ‘orange’ }).checked(false) ← 同组自动取消5.2 State 驱动的 UI 更新当 State selectedTheme 变化时所有依赖它的 UI 表达式重新求值.checked(this.selectedTheme item.value) → 新选中项变为 true其他变为 false.border({ color: … }) → 选中项显示高亮边框.fontColor(…) / .fontWeight(…) → 选中项文字变色加粗Text(‘已选: …’) → 底部提示文字更新汇总区的 buildResultRow 文本更新这就是 ArkTS 声明式 UI 的核心优势状态变化自动触发所有依赖项的更新无需手动操作 DOM。六、样式自定义技巧6.1 修改 Radio 指示器颜色Radio 的圆点颜色可以通过 IndicatorStyle 自定义Radio({ value: ‘option’, group: ‘g’, indicator: {primaryColor: ‘#317AF7’, // 选中时的主色secondaryColor: ‘#E0E0E0’ // 未选中时的边框色}})6.2 隐藏原生 Radio完全自定义视觉如 buildSizeGroup 所示将 Radio 设为极小并透明然后用外层容器的样式来表达选中状态。这种方案适用于需要完全掌控视觉效果的场景。// 隐藏 RadioRadio({ value: item.value, group: ‘sizeGroup’ }).checked(this.selectedSize item.value).width(1).height(1).opacity(0).onChange((isChecked: boolean) {if (isChecked) { this.selectedSize item.value; }})// 用外层卡片的样式表示选中Stack().backgroundColor(this.selectedSize item.value ? ‘#E8F0FE’ : ‘#FFFFFF’).border({ color: this.selectedSize item.value ? ‘#317AF7’ : ‘#E0E0E0’, … })原理 Radio 的逻辑互斥、选中、分组仍然由框架管理但它的视觉层被覆盖完全由自定义样式替代。6.3 整行点击增强使用外层 Row 或 Stack 的 .onClick() 来扩大点击热区Row().width(‘90%’).height(64) // 更大的点击区域.onClick(() { this.selectedPay item.value; }){// … 图标、文字、Radio}这显著提升了用户体验尤其在移动设备上——用户的手指无需精确对准 Radio 圆点。6.4 使用 Emoji 作为图标在 PayItem 中使用了 Emoji 作为图标。这是 ArkTS 的一个巧妙用法——Text 组件原生支持 Emoji 渲染无需加载任何图片资源。配合 fontSize(28) 即可获得清晰的大图标。七、最佳实践与注意事项7.1 Radio 分组命名的规范group 属性是 Radio 分组的唯一标识。建议遵循以下规范作用域清晰使用页面/功能名称做前缀避免全局冲突。如 ‘themeGroup’、‘payGroup’、‘sizeGroup’避免动态 group 名group 值一旦确定不要运行时改变否则 Radio 会脱离原组保持 group 字符串一致同一个组内的所有 Radio 的 group 值必须完全一致包括大小写7.2 value 的编码规范使用有意义的字符串如 ‘wechat’、‘alipay’ 比 ‘1’、‘2’ 更可读避免使用特殊字符value 可能会被用于 JSON 序列化或日志输出保持唯一性同一个 group 内所有 Radio 的 value 必须两两不同7.3 onChange 的防重复更新在某些场景中onChange 可能会被多次触发如快速点击。但 ArkTS 的 State 更新是幂等的——当设置的值与当前值相同时框架不会触发重新渲染。因此以下写法是安全的.onChange((isChecked: boolean) {if (isChecked) {this.selectedTheme item.value; // 即使被调用两次值相同也不会重复渲染}})7.4 在 List 中使用 Radio当 Radio 在 List 组件的 ListItem 中使用时需要注意List() {ForEach(this.options, (item: OptionItem) {ListItem() {Row() {Text(item.label)Spacer()Radio({ value: item.value, group: ‘listGroup’ }).checked(this.selected item.value).onChange((isChecked: boolean) {if (isChecked) { this.selected item.value; }})}}})}注意事项group 值在列表内所有 Radio 中保持一致每个 ListItem 中的 Radio value 必须唯一列表滚动时 Radio 的选中状态不会丢失由 State 管理7.5 无障碍访问鸿蒙的 Radio 组件默认支持无障碍功能。如果需要进一步提升无障碍体验可以添加 accessibilityTextRadio({ value: ‘wechat’, group: ‘payGroup’ }).accessibilityText(‘微信支付’)八、常见错误与解决方案8.1 忘记指定 group 属性// ❌ 错误没有 groupRadio 无法实现互斥Radio({ value: ‘a’ })Radio({ value: ‘b’ })// ✅ 正确指定相同 group两个 Radio 互斥Radio({ value: ‘a’, group: ‘g1’ })Radio({ value: ‘b’, group: ‘g1’ })如果没有 group 属性每个 Radio 都是独立存在的可以同时被选中——这就失去了单选框的意义。8.2 不同组使用了相同的 group 名称// ❌ 错误两组 Radio 的 group 都是 ‘size’它们会互相干扰Radio({ value: ‘small’, group: ‘size’ }) // 第一组Radio({ value: ‘medium’, group: ‘size’ })Radio({ value: ‘large’, group: ‘size’ })Radio({ value: ‘xs’, group: ‘size’ }) // 第二组错误地用了同组名// ✅ 正确使用不同的 group 名称Radio({ value: ‘small’, group: ‘drinkSize’ })Radio({ value: ‘medium’, group: ‘drinkSize’ })Radio({ value: ‘large’, group: ‘drinkSize’ })Radio({ value: ‘xs’, group: ‘shirtSize’ })8.3 onChange 中没有判断 isChecked// ❌ 错误没有判断 isChecked取消选中时也会错误地更新状态Radio({ value: ‘a’, group: ‘g’ }).onChange((isChecked: boolean) {this.selected ‘a’; // 取消时也会执行})// ✅ 正确只在选中时更新Radio({ value: ‘a’, group: ‘g’ }).onChange((isChecked: boolean) {if (isChecked) { this.selected ‘a’; }})当一个 Radio 被选中时同组其他 Radio 会收到 onChange 调用且 isChecked 为 false。如果不判断 isChecked就会错误地更新状态。8.4 使用 RadioGroup 但组件不存在在某些 HarmonyOS NEXT 版本中RadioGroup 容器组件并未提供。如果代码中使用了 RadioGroup会遇到 Cannot find name ‘RadioGroup’ 编译错误。解决方案 放弃使用 RadioGroup直接在每个 Radio 上通过 group 属性完成分组。九、与其他表单组件的配合9.1 Radio Button 提交我们的示例在底部添加了提交选择按钮展示了如何将 Radio 的选中值用于表单提交Button(‘提交选择’).onClick(() {promptAction.showToast({message:提交成功主题:${themeLabel} | 支付:${payLabel} | 尺寸:${sizeLabel},duration: 3000});})实际项目中这里通常会调用网络请求或路由跳转。9.2 Radio TextInput 条件显示Radio 的选中值可以控制其他组件的显示与隐藏Radio({ value: ‘custom’, group: ‘inputGroup’ }).checked(this.inputMode ‘custom’)if (this.inputMode ‘custom’) {TextInput({ placeholder: ‘请输入自定义内容’ }).width(‘90%’).margin({ top: 8 })}9.3 Radio Select 联动Radio 的选中值可以作为下拉框的选项数据源Select([{ value: ‘1’, icon: ‘’ }, { value: ‘2’, icon: ‘’ }]).selected(this.selectedTheme ‘blue’ ? 0 : 1)十、性能优化建议10.1 使用 ForEach 替代手写当 Radio 选项来自数组时始终使用 ForEach 遍历生成而不是手写多个 Radio 组件。这样不仅代码更简洁还能利用 State Prop 的响应式更新机制。10.2 避免在 onChange 中执行耗时操作onChange 回调中应只做状态更新操作。如果需要触发网络请求或复杂计算建议使用异步操作或放到 aboutToAppear 等生命周期方法中。10.3 减少不必要的状态更新当用户点击已经选中的 Radio 时onChange 仍然会被调用。可以通过判断当前值来避免不必要的更新.onChange((isChecked: boolean) {if (isChecked this.selectedTheme ! item.value) {this.selectedTheme item.value;}})不过 ArkTS 的 State 赋值已经做了幂等处理——设置相同值不会触发重新渲染所以这一优化的实际收益很有限。十一、总结11.1 核心要点回顾Radio group value 是单选组的三要素group 定义分组实现互斥value 标识每个选项数据驱动选中状态.checked() 绑定 State 变量与 value 的比较结果onChange 回调只在 isChecked true 时更新对应的状态变量三种布局形态横向 Row、纵向 Column、卡片 Stack满足不同 UI 需求自定义样式方案透明隐藏 Radio 后用外层容器样式表达选中状态实现完全自定义点击热区增强外层容器的 .onClick() 让用户整行可点11.2 适用原则单选框的最佳实践是数据驱动选中状态整行可点增强体验自定义样式满足视觉需求。Radio 虽然是一个很小的组件但在表单和多选一场景中无处不在。掌握它的布局技巧和数据绑定模式能让你的鸿蒙应用交互更加流畅、视觉更加统一。11.3 完整代码本文对应的完整代码位于entry/src/main/ets/pages/RadioDemo.ets480 行在 DevEco Studio 中打开项目运行后首页点击 Radio 单选框编号 03即可查看三种 Radio 布局演示切换选项后底部汇总区自动更新点击提交选择按钮弹出确认提示。运行环境 HarmonyOS NEXT API 12 / DevEco Studio 5.0本文由 AtomCodedeepseek-v4-flash辅助完成。