前言
手机里总有一些东西是“看一眼就想删”的。浏览器的历史记录、购物车的过期商品、备忘录里半年前写的草稿。如果一条一条删,光是点删除按钮再确认,就足够让人烦躁得想把手机扔出窗外。但如果是手势左滑删除,速度会快不少;如果能多选几条然后一键清空,那种快感就更强烈了——就像一口气撕掉冰箱上所有过期的便签,爽快。

我一直觉得,“多选删除”是列表控件从“能用”到“好用”的一道分水岭。它不复杂,但实现起来涉及状态同步、复选框的批量联动、以及删除时的数组安全性。那天中午,我打开 DevEco Studio 6.1.1 Beta1,在 Pura X Max 模拟器上,用 Checkbox、List 和 @State 数组搭了一个待办事项列表,并给它装上了全选、反选和一键删除的能力。这篇文章就是那次实践的完整记录——不只是给你一份能跑的代码,更想聊聊这个看似简单的功能背后,藏着哪些让人“踩坑”的设计细节。
一、给每条待办配一个“选票”——Checkbox 怎么和数组绑定
多选删除的第一步,是给列表里每一项前面加一个复选框。HarmonyOS 提供了 Checkbox 组件,它能独立管理自己的选中状态,但我们需要一个“中央控制器”来记录所有项的选中情况。这个控制器就是一个布尔数组 checkedStates,长度和待办列表一样,每一项对应一个布尔值,表示该待办是否被勾选。

在 List 组件里,我们用 ForEach 遍历待办列表 todoItems 和对应的 checkedStates。每一行用一个 Row 包起来,左侧是 Checkbox,右侧是待办文字。Checkbox 的 checked 属性绑定到 checkedStates[index],onChange 回调里更新对应的布尔值。这种绑定方式让点击复选框和点击列表项本身变成了两件独立的事:点复选框只改变选中状态,不触发其他操作;点列表项可以进入详情编辑(或者什么也不做)。
但这里有一个常见的需求:用户点击列表项本身时,也应该能选中或取消选中。我们可以在 ListItem 外面再包一层点击事件,或者让整个行响应点击,在点击回调里切换 checkedStates[index]。这样用户不需要精准戳那个小方框,点整行就能选中,体验会好很多。我选择了后者:整个 Row 的 onClick 事件用来切换选中状态,而 Checkbox 的 onChange 只是同步更新数组,不额外做别的。这样无论用户点复选框还是点文字,都能达到选中/取消的目的。
代码里,checkedStates 是一个 @State 修饰的布尔数组。Checkbox 的 checked 用 this.checkedStates[index] 绑定,onChange 里直接用索引更新对应位置的值。ArkUI 会检测到数组元素变化并刷新界面。这个数组是整个过程的核心状态,后面的全选、删除,全都围绕它来操作。
二、全选与取消全选——一个按钮的两种面孔
既然每一条都有复选框,用户自然会期待有一个“全选”按钮。点一下,所有复选框都勾上;再点一下,全部取消。这个按钮就放在列表上方,用一个独立的 Row 来安放。按钮的文字根据当前状态动态变化:如果所有待办都被选中,显示“取消全选”;否则显示“全选”。

判断“是否全选”的方法很直接:检查 checkedStates 数组是否所有值都为 true。如果待办列表为空,全选按钮应该置灰不可用。全选的逻辑就是把 checkedStates 的每一个元素都设为 true,取消全选就是全部设为 false。由于 checkedStates 是 @State 数组,修改后整个列表会自动重绘,所有 Checkbox 组件都会同步更新选中状态。
这里有一个性能相关的考量:如果待办事项非常多(比如几百条),全选操作会一次性更新几百个 @State 元素,但 ArkUI 的渲染引擎会批量处理这些更新,界面只会刷新一次,不会出现逐条闪动的现象。所以即使列表很长,全选操作在模拟器上也是瞬间完成的。
除了全选,我们还提供了一个“删除选中”按钮。它的文字上会动态显示“删除选中 (X)”,X 是当前被选中的条数。如果选中数为 0,按钮置灰禁用,避免无效点击。这种动态反馈让用户知道自己选中了多少条,也避免了“明明没选中却点删除”的空操作。
三、批量删除——splice 的倒序技巧
删除操作是所有状态更新里最容易出 bug 的。假设我们从前往后遍历 checkedStates,找到第一个 true 的位置,删除对应的待办和选中状态,然后继续遍历——这时候整个数组的长度和索引已经变了,原来的索引偏移了一位,后面的元素会被跳过。这是初学者很容易踩的坑。
解决办法是从后往前遍历。从数组末尾开始检查,遇到 checkedStates[i] 为 true,就同时从 todoItems 和 checkedStates 中 splice(i, 1)。因为从后往前删,前面元素的索引不会受影响,所以不会漏删。另一种做法是用 filter 创建新数组,保留未被选中的项,代码更简洁,性能也不错。但对于小规模列表(几十条),两种方法都可行。
我选择了 filter 方案,因为它更符合函数式编程的习惯,且代码量少:遍历 checkedStates,筛选出那些为 false 的索引,生成新的 todoItems 和 checkedStates。由于 @State 数组替换会触发 UI 刷新,界面瞬间更新。
删除后,全选按钮的状态也需要重新计算。因为删除后所有元素都不再被选中(或者极少数元素选中),全选按钮会自动回到“全选”文字。如果列表被删空了,全选按钮和删除按钮都禁用,页面显示“暂无待办”的占位提示。
为了让删除操作更安全,我在界面上加了一个隐形的确认机制:删除按钮不会直接触发删除,而是先用 AlertDialog 弹窗确认。虽然这会增加一步操作,但能有效防止误触。如果用户觉得烦,可以把确认弹窗去掉,或者在代码里加一个“撤销”功能。为了演示简洁,我这里使用了直接删除(在按钮点击时用 filter 更新数组),没有弹确认框。读者可以根据自己的喜好添加。
四、让列表有初始内容——示例数据与添加功能

一个空的待办列表不太直观,所以我在组件初始化时塞了几条示例数据。这些示例数据写死在 aboutToAppear 里,包含一些日常琐事,让列表在打开时就有内容可以操作。同时,我还加了一个简单的“添加”功能:上方一个 TextInput 和一个“添加”按钮,输入新待办后追加到列表末尾。添加时,对应的 checkedStates 也追加一个 false,保持长度一致。
添加后,列表自动更新,新项出现在底部。如果列表很长,可以配合之前的删除操作,把不需要的旧待办清掉。
这样,这个工具就不仅仅是一个“多选删除”的演示,更是一个微型待办清单的基础骨架。用户可以添加、勾选、批量删除,已经具备了基本的生产力属性。
五、完整代码——一个能打勾、能全选、能批量删的列表
以下代码适配 DevEco Studio 6.1.1 Beta1、SDK22 语法,Pura X Max 模拟器。新建 Empty Ability 项目,把 entry/src/main/ets/pages/Index.ets 全部替换即可。无需任何权限,纯组件搭建。

/*
* 多选删除列表 — Checkbox + List + 批量删除
* 环境:DevEco Studio 6.1.1 Beta1,Pura X Max 模拟器,SDK22
*/
@Entry
@Component
struct Index {
@State todoItems: string[] = [];
@State checkedStates: boolean[] = [];
@State newItem: string = '';
async aboutToAppear(): Promise<void> {
// 初始化示例数据
this.todoItems = ['买牛奶', '写周报', '跑步30分钟', '还书给图书馆', '预定会议室'];
this.checkedStates = new Array(this.todoItems.length).fill(false);
}
// 获取选中项数量
private getSelectedCount(): number {
return this.checkedStates.filter((v) => v).length;
}
// 是否全选
private isAllSelected(): boolean {
return this.todoItems.length > 0 && this.checkedStates.every((v) => v);
}
// 切换某一条的选中
private toggleCheck(index: number): void {
this.checkedStates[index] = !this.checkedStates[index];
// 触发数组更新
this.checkedStates = [...this.checkedStates];
}
// 全选/取消全选
private toggleAll(): void {
let allSelected = this.isAllSelected();
let newStates = new Array(this.todoItems.length).fill(!allSelected);
this.checkedStates = newStates;
}
// 删除选中项
private deleteSelected(): void {
let newItems: string[] = [];
let newStates: boolean[] = [];
for (let i = 0; i < this.todoItems.length; i++) {
if (!this.checkedStates[i]) {
newItems.push(this.todoItems[i]);
newStates.push(false);
}
}
this.todoItems = newItems;
this.checkedStates = newStates;
}
// 添加新项
private addItem(): void {
let text = this.newItem.trim();
if (text === '') return;
this.todoItems = [...this.todoItems, text];
this.checkedStates = [...this.checkedStates, false];
this.newItem = '';
}
build() {
Column() {
// 标题
Text('待办清单')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 8 })
Text('勾选后一键删除,支持全选')
.fontSize(15)
.fontColor('#888')
.margin({ bottom: 15 })
// 添加新项
Row() {
TextInput({ placeholder: '添加新待办', text: this.newItem })
.onChange((v: string) => { this.newItem = v; })
.layoutWeight(1)
.fontSize(16)
Button('添加')
.type(ButtonType.Capsule)
.fontSize(15)
.backgroundColor('#1976D2')
.fontColor(Color.White)
.margin({ left: 8 })
.onClick(() => { this.addItem(); })
}
.width('90%')
.margin({ bottom: 12 })
// 操作栏
Row() {
Button(this.isAllSelected() ? '取消全选' : '全选')
.type(ButtonType.Capsule)
.fontSize(14)
.backgroundColor('#EEEEEE')
.fontColor('#333')
.onClick(() => { this.toggleAll(); })
Blank()
Button(`删除选中 (${this.getSelectedCount()})`)
.type(ButtonType.Capsule)
.fontSize(14)
.backgroundColor(this.getSelectedCount() > 0 ? '#F44336' : '#CCCCCC')
.fontColor(Color.White)
.onClick(() => {
if (this.getSelectedCount() > 0) {
this.deleteSelected();
}
})
}
.width('90%')
.margin({ bottom: 10 })
// 待办列表
if (this.todoItems.length === 0) {
Text('暂无待办,添加一条吧')
.fontSize(16)
.fontColor('#BBB')
.margin({ top: 60 })
} else {
List() {
ForEach(this.todoItems, (item: string, index: number) => {
ListItem() {
Row() {
Checkbox()
.checked(this.checkedStates[index])
.onChange((value: boolean) => {
this.checkedStates[index] = value;
this.checkedStates = [...this.checkedStates];
})
Text(item)
.fontSize(17)
.fontColor('#333')
.margin({ left: 10 })
.layoutWeight(1)
}
.width('100%')
.padding({ top: 12, bottom: 12, left: 16, right: 16 })
.backgroundColor('#FFFFFF')
.borderRadius(8)
.margin({ bottom: 6 })
.onClick(() => { this.toggleCheck(index); })
}
}, (item: string, index: number) => item + index)
}
.width('90%')
.layoutWeight(1)
}
Text('💡 点击整行或复选框选中,支持全选与批量删除')
.fontSize(12)
.fontColor('#AAA')
.width('90%')
.textAlign(TextAlign.Center)
.margin({ top: 10, bottom: 10 })
}
.width('100%')
.height('100%')
.backgroundColor('#FAFAFA')
}
}
这份代码实现了一个标准的多选删除列表。添加、全选、单条勾选、批量删除,所有操作都实时反映在界面上。checkedStates 数组和 todoItems 数组保持同步,删除时用 filter 方式重建两个数组,保证索引不偏移。全选按钮的文字根据当前状态动态变化,删除按钮显示选中数量。
六、运行效果
代码粘贴进 DevEco Studio,Run 到 Pura X Max 模拟器。屏幕上出现一个待办清单,预置了五条任务。每条前面有一个小方框,点击方框或整行都可以勾选。随便勾选两三条,上方的“删除选中”按钮上会实时显示选中数量,比如“删除选中 (2)”。点一下“全选”,所有复选框瞬间勾满,按钮变成“取消全选”。再点“删除选中”,被勾选的项从列表里消失,其他项自动上移。添加新待办,新条目出现在列表底部。整个操作行云流水,没有延迟,也没有任何选中状态残留的 bug。


总结
这个多选删除列表,虽然只是一个列表控件的简单变体,却把声明式 UI 开发中最常用的几个技能点覆盖了:
- Checkbox 组件的状态绑定:用
@State数组管理批量复选框,每一个Checkbox通过索引读取自己的选中状态,更新时同步写回数组。 - 数组的安全操作:通过
filter或倒序splice实现批量删除,避免了索引偏移导致的漏删或错删问题,这是数据处理的基本功。 - 全选/取消全选的动态逻辑:通过
every方法判断是否全选,用一个布尔值驱动按钮的文字和功能切换,代码清晰且易于扩展。 - 声明式 UI 的便利性:无论是勾选、删除还是添加,所有操作都只修改
@State数据,界面自动响应变化,无需手动刷新列表。
这个骨架稍加扩展,就能变成购物车、邮件收件箱、文件管理器等任何需要“批量操作”的场景。下次你在手机里看到某个应用的多选删除功能,大概能猜到它背后的代码长什么样——不过是一个 checkedStates 数组,和一个安全的 filter 而已。而正是这些不起眼的小设计,让我们的数字生活变得稍微高效了一点点。
8526

被折叠的 条评论
为什么被折叠?



