当手机里的待办事项堆积如山——我在 HarmonyOS 上给列表装了个多选删除功能

📅 2026/6/25 14:40:58
当手机里的待办事项堆积如山——我在 HarmonyOS 上给列表装了个多选删除功能
前言手机里总有一些东西是“看一眼就想删”的。浏览器的历史记录、购物车的过期商品、备忘录里半年前写的草稿。如果一条一条删光是点删除按钮再确认就足够让人烦躁得想把手机扔出窗外。但如果是手势左滑删除速度会快不少如果能多选几条然后一键清空那种快感就更强烈了——就像一口气撕掉冰箱上所有过期的便签爽快。我一直觉得“多选删除”是列表控件从“能用”到“好用”的一道分水岭。它不复杂但实现起来涉及状态同步、复选框的批量联动、以及删除时的数组安全性。那天中午我打开 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 Beta1Pura X Max 模拟器SDK22 */ Entry Component struct Index { State todoItems: string[] []; State checkedStates: boolean[] []; State newItem: string ; async aboutToAppear(): Promisevoid { // 初始化示例数据 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 StudioRun 到 Pura X Max 模拟器。屏幕上出现一个待办清单预置了五条任务。每条前面有一个小方框点击方框或整行都可以勾选。随便勾选两三条上方的“删除选中”按钮上会实时显示选中数量比如“删除选中 (2)”。点一下“全选”所有复选框瞬间勾满按钮变成“取消全选”。再点“删除选中”被勾选的项从列表里消失其他项自动上移。添加新待办新条目出现在列表底部。整个操作行云流水没有延迟也没有任何选中状态残留的 bug。总结这个多选删除列表虽然只是一个列表控件的简单变体却把声明式 UI 开发中最常用的几个技能点覆盖了Checkbox 组件的状态绑定用State数组管理批量复选框每一个Checkbox通过索引读取自己的选中状态更新时同步写回数组。数组的安全操作通过filter或倒序splice实现批量删除避免了索引偏移导致的漏删或错删问题这是数据处理的基本功。全选/取消全选的动态逻辑通过every方法判断是否全选用一个布尔值驱动按钮的文字和功能切换代码清晰且易于扩展。声明式 UI 的便利性无论是勾选、删除还是添加所有操作都只修改State数据界面自动响应变化无需手动刷新列表。这个骨架稍加扩展就能变成购物车、邮件收件箱、文件管理器等任何需要“批量操作”的场景。下次你在手机里看到某个应用的多选删除功能大概能猜到它背后的代码长什么样——不过是一个checkedStates数组和一个安全的filter而已。而正是这些不起眼的小设计让我们的数字生活变得稍微高效了一点点。