# CMP for OpenHarmony:Checkbox 多选框的“集合状态机”实现——三态全选 + 批量操作 + 多弹窗入口(项目实战代码)
代码地址:通过网盘分享的文件:cmp_openharmony.zip链接: https://pan.baidu.com/s/15rN1LvJ0KENMkYZfLq_R1Q?pwd=nhqe 提取码: nhqelocked。
CMP for OpenHarmony:Checkbox 多选框的“集合状态机”实现——三态全选 + 批量操作 + 多弹窗入口(项目实战代码)
代码地址:通过网盘分享的文件:cmp_openharmony.zip
链接: https://pan.baidu.com/s/15rN1LvJ0KENMkYZfLq_R1Q?pwd=nhqe 提取码: nhqe
多选框(Checkbox)在工程里真正麻烦的部分通常不是“画一个勾”,而是:
- 如何表达“选中集合”(而不是一个个 boolean)
- 如何实现“全选/清空/反选/预设选择”这种批量动作
- 如何展示“部分选中”(也就是全选按钮的半选态)
- 当页面有多个入口(主界面、弹窗、抽屉、菜单、底部弹层)都能改选中集合时,如何避免状态分裂
本文基于本仓库真实可运行代码,给出一套可直接复用的 Checkbox 组织方式:
- 以
selectedIds: Set<String>作为单一数据源 - 用
TriStateCheckbox展示“全选/半选/未选”汇总态 - 用
locked统一拦截“拖动/点击输入 + 外部入口改写” - 用 Modal / Drawer / Popover / BottomSheet 四类入口对同一份集合做批量操作
1. 代码位置:示例页在哪
本文所有 Kotlin 代码均来自下面文件(复制路径即可定位):
composeApp/src/commonMain/kotlin/com/tencent/compose/sample/checkbox/MultiPopupCheckboxDemoPage.kt
2. 选中集合:用 Set<String> 表达“多选状态”
示例页将多选结果收口为一个集合(节选,保持项目原样):
var selectedIds by remember { mutableStateOf(setOf<String>()) }
var locked by remember { mutableStateOf(false) }
这里的核心思想是:
- 多选的“本体状态”应该是集合(哪些 id 被选中),而不是一堆零散的
var checkedA/var checkedB。 - 集合更适合承载批量操作:全选就是把所有 id 放进集合;清空就是置空集合;反选就是做集合差。
locked属于页面策略:当锁定时,所有入口都不应该再改集合。
3. 三态全选:TriStateCheckbox 的状态必须由集合推导
示例页用 derivedStateOf 从 selectedIds 推导 triState(节选,保持项目原样):
val triState by remember(options, selectedIds) {
derivedStateOf {
when {
selectedIds.isEmpty() -> ToggleableState.Off
selectedIds.size == options.size -> ToggleableState.On
else -> ToggleableState.Indeterminate
}
}
}
这段代码解决的是“全选按钮如何正确展示半选态”的问题:
- 未选(Off):集合为空。
- 全选(On):集合大小等于总选项数。
- 部分选中(Indeterminate):既不为空,也不满。
为什么要用推导而不是再维护一个 var allChecked?
- 因为
allChecked很容易与selectedIds不一致(尤其当选择入口变多:列表点选、抽屉按钮、弹层预设、Popover 菜单等)。 - 推导能保证:只要
selectedIds对了,三态展示就永远正确。
4. 锁定拦截:统一封装成 applyIfUnlocked,避免每个入口重复写 if
示例页提供了一个小工具函数(节选,保持项目原样):
fun applyIfUnlocked(block: () -> Unit): Boolean {
if (locked) {
lastResult = "已锁定,忽略操作"
return false
}
block()
return true
}
这段代码的价值在于:
- 统一策略:所有入口都通过它判断是否允许写状态。
- 减少重复:不用在每个按钮/弹窗/菜单里重复写
if (locked) return。 - 可观测性:锁定时顺便更新
lastResult,Demo 页面可以直观看到“为什么没生效”。
5. 批量动作:全选 / 清空 / 反选其实就是对集合的三种变换
示例页把批量操作实现成三个函数(节选,保持项目原样):
fun selectAll() {
selectedIds = options.map { it.id }.toSet()
}
fun clearAll() {
selectedIds = emptySet()
}
fun invertSelection() {
val all = options.map { it.id }.toSet()
selectedIds = all - selectedIds
}
这里的可复用点:
- 全选:把所有 id 变成一个集合。
- 清空:直接置空集合。
- 反选:用全集
all减去当前选中集合。
这种写法的优势是清晰、无副作用、也不依赖 UI 组件;同一套逻辑可以被任何入口复用。
6. 列表项点击:只维护 selectedIds,不要给每项建第二份状态
示例页在列表里切换某一项选中状态(节选,保持项目原样):
selectedIds = if (selectedIds.contains(option.id)) {
selectedIds - option.id
} else {
selectedIds + option.id
}
解释要点:
- 选中 -> 取消:从集合中删除 id。
- 未选 -> 选中:向集合中添加 id。
注意这里没有出现 option.checked = ... 之类的字段,因为选项本身是静态数据(id + label),选中状态应该由 selectedIds.contains(id) 推导。
7. 顶部全选点击:三态按钮的点击语义要和状态推导保持一致
三态按钮点击逻辑(节选,保持项目原样):
TriStateCheckbox(
state = triState,
onClick = {
applyIfUnlocked {
when (triState) {
ToggleableState.On -> clearAll()
else -> selectAll()
}
}
lastResult = when (triState) {
ToggleableState.On -> "顶部:取消全选"
ToggleableState.Off -> "顶部:全选"
ToggleableState.Indeterminate -> "顶部:全选"
}
},
enabled = !locked
)
这里的设计边界是:
- 当前是 全选(On)时点击 -> 执行清空。
- 当前是 未选/半选(Off/Indeterminate)时点击 -> 执行全选。
这样用户操作的预期更稳定:
- 只要不是全选,点一次就全选
- 只有在全选态时,点一次才是取消全选
8. 列表行点击与 Checkbox 点击:避免“双触发”
示例页把整行设置为可点击,同时将 Checkbox 的 onCheckedChange 设为 null(节选,保持项目原样):
Row(
modifier = Modifier
.clickable(
enabled = enabled,
indication = null,
interactionSource = remember { MutableInteractionSource() }
) { onToggle() }
) {
Checkbox(
checked = checked,
onCheckedChange = null,
enabled = enabled
)
}
这段代码解决的是一个很常见的交互坑:
- 如果行可点击、Checkbox 也可点击,并且两边都触发
onToggle(),用户点在 Checkbox 上可能会触发两次,导致“看起来没变化”。 - 通过把 Checkbox 的
onCheckedChange置为null,让“点击源”只有行本身,从而保证一次点击只触发一次状态切换。
9. 多入口控制:同一份集合被 Modal / Drawer / Popover / BottomSheet 改写
9.1 Drawer(抽屉)适合放“批量控制面板”
DrawerContent 中把全选/清空/反选集中(节选,保持项目原样):
onSelectAll = {
if (applyIfUnlocked({ selectAll() })) lastResult = "抽屉:全选"
},
onClear = {
if (applyIfUnlocked({ clearAll() })) lastResult = "抽屉:清空"
},
onInvert = {
if (applyIfUnlocked({ invertSelection() })) lastResult = "抽屉:反选"
}
这里的关键点:
- 抽屉按钮并不直接操作 UI,只是调用集合变换函数。
- 写入之前统一走
applyIfUnlocked,把锁定策略收口。
9.2 BottomSheet / Popover:锁定时“提前返回”要写对作用域
BottomSheet 的回调(节选,保持项目原样):
onAction = sheetAction@{ label ->
showBottomSheet = false
if (locked && label != "关闭") {
lastResult = "BottomSheet:已锁定,忽略操作"
return@sheetAction
}
// ...
}
Popover 的回调(节选,保持项目原样):
onAction = popAction@{ action ->
showPopover = false
if (locked && action != "关闭") {
lastResult = "Popover:已锁定,忽略操作"
return@popAction
}
// ...
}
解释要点:
- 当你在 lambda 中需要提前退出时,建议像示例这样用
sheetAction@/popAction@显式标记作用域,再return@label。 - 这样可以避免嵌套 lambda 场景下 return 作用域不明确导致的编译/逻辑问题。
10. 如何在工程里直接体验该示例
建议做法与其它 Demo 一样:打开入口文件并临时切换渲染页面。
- 入口文件:
composeApp/src/commonMain/kotlin/com/tencent/compose/sample/mainpage/MainPage.kt - 将入口渲染切换到:
com.tencent.compose.sample.checkbox.MultiPopupCheckboxDemoPage()
(这里不贴固定代码块,避免入口频繁切换导致文内代码与仓库不一致。)
11. 自检清单:Checkbox 在工程里最容易踩的坑
- 三态必须推导:
TriStateCheckbox的状态一定要从selectedIds推导,避免双状态。 - 批量操作要落到集合变换:全选/清空/反选用函数收口,别散落在各个按钮里。
- 锁定要统一:锁定后不仅禁用点击,还要拦截弹窗/菜单/弹层写入。
- 避免双触发:行可点时,Checkbox 本体建议
onCheckedChange = null。 - 提前返回写对作用域:用
sheetAction@/popAction@明确return@label。

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)