鸿蒙原生ArkTS布局方式之List+multipleSelect多选列表
鸿蒙原生ArkTS布局方式之List+multipleSelect多选列表


一、概述
在鸿蒙 HarmonyOS NEXT(API 12+)应用开发中,列表(List)是最常见、最重要的 UI 容器之一。无论是社交应用的消息流、电商应用的商品列表,还是办公应用的任务看板,几乎所有涉及数据集合展示的场景都离不开列表组件。而当列表需要与用户进行批量交互——例如多选删除、批量标记、批量移动——时,「List + multipleSelect 多选列表」布局模式便成为必不可少的解决方案。
本文将以一个完整的「待办事项多选删除」示例应用为主线,深入剖析鸿蒙原生 ArkTS 框架中 List、ListItem、Checkbox 以及 selectable / onSelect 机制的组合使用方式。文章涵盖从基础概念到高级实践的完整知识体系,旨在帮助开发者系统掌握鸿蒙多选列表的布局设计与开发技能。
1.1 适用场景
List + multipleSelect 布局模式适用于以下典型场景:
- 消息/邮件管理:批量标记已读、批量删除、批量归档
- 文件管理器:多选文件进行复制、移动、压缩、删除
- 购物车/订单管理:批量结算、批量移除商品
- 权限/角色管理:批量分配或回收权限
- 数据清洗工具:批量筛选、批量标注
- 学习/考试系统:批量选题、批量评分
1.2 技术栈概览
| 技术组件 | 角色说明 |
|---|---|
List |
高性能虚拟滚动列表容器,负责布局管理与复用优化 |
ListItem |
列表项容器,承载每一项的具体内容 |
.selectable(true) |
ListItem 属性,开启选中交互能力 |
.onSelect() |
ListItem 选中状态变化回调 |
Checkbox |
复选框组件,直观展示选中/未选状态 |
ForEach |
循环渲染指令,绑定数据源与 UI |
@State |
状态装饰器,驱动 UI 响应式刷新 |
Set<number> |
选中 ID 集合,高效管理多选状态 |
promptAction.showDialog |
确认弹窗,防止误操作 |
二、核心组件详解
2.1 List 组件
List 是 ArkUI 框架中提供的高性能滚动列表容器,底层基于虚拟滚动技术实现在线渲染。与传统的 Scroll + Column 组合相比,List 只会渲染当前视口内的可见列表项,当用户滚动时动态回收不可见节点并复用,因此能够轻松支撑成千上万条数据的高效展示。
List 的核心属性:
List({ space: number, initialIndex: number, scroller: Scroller })
- space:列表项之间的间距,单位为 vp。
- initialIndex:初始滚动到的项索引(从 0 开始)。
- scroller:滚动控制器,可用于编程控制滚动位置(如回到顶部)。
布局方向:List 默认纵向排列。通过 .listDirection() 可改为横向。
2.2 ListItem 组件
ListItem 是 List 的直接子组件,代表列表中的单个条目。它是多选交互的核心载体。
2.2.1 selectable 属性
selectable 是 ListItem 独有的布尔属性,控制该列表项是否参与选中交互:
ListItem()
.selectable(true) // 启用选中功能
当 selectable(true) 时,ListItem 会:
- 监听用户的点击/触摸事件
- 维护内部的选中状态开关
- 在状态变化时触发
onSelect回调
当 selectable(false)(默认值)时,ListItem 忽略选中逻辑,点击事件直接透传到其内部的子组件。
2.2.2 onSelect 回调
onSelect 是 ListItem 提供的事件回调,当用户触摸某个开启了 selectable 的列表项时触发:
ListItem()
.selectable(true)
.onSelect((isSelected: boolean) => {
// isSelected: true → 选中, false → 取消选中
})
重要行为特征:
onSelect在每次选中/取消选中时都会触发- 回调携带的
isSelected参数是ListItem内部维护的选中状态 - 点击
ListItem的任何区域都会触发(不仅仅是 Checkbox) onSelect与onClick可能同时触发,两者不互斥
2.3 Checkbox 组件
Checkbox(复选框)用于直观地展示选中状态,并提供用户交互入口:
Checkbox()
.select(boolean) // 设置选中/未选中
.selectedColor(Color) // 选中时的填充颜色
.shape(CheckBoxShape) // 形状:圆形或圆角方形
.mark({ strokeColor, size, width }) // 勾选标记样式
.onChange((value: boolean) => {}) // 切换时回调
在多选列表中,Checkbox 与 ListItem 的 selectable 机制形成互补:
ListItem.selectable(true) + onSelect:负责捕获用户交互事件Checkbox:负责视觉反馈,展示当前状态
2.4 ForEach 循环渲染
ForEach 是 ArkTS 中用于从数组数据源循环生成 UI 组件的指令:
ForEach(
arr: any[], // 数据源
itemGenerator: (item, index) => void, // 项生成器
keyGenerator?: (item, index) => string // 键值生成器(用于列表复用优化)
)
在多选列表中,ForEach 的 keyGenerator 尤为关键。它返回每个列表项的唯一标识符,帮助框架在数据变化时精确识别哪些项需要添加、移除或更新。通常使用数据实体的 id 字段作为 key:
ForEach(this.todoList,
(item: TodoItem) => { ... },
(item: TodoItem) => item.id.toString()
)
三、数据模型与状态设计
3.1 数据模型定义
良好的数据模型设计是多选列表功能正确运行的基础。我们使用 interface 定义数据结构:
interface TodoItem {
id: number; // 唯一标识(主键)
title: string; // 事项标题
time: string; // 创建/截止时间
tag: string; // 标签分类
}
关键设计原则:
id必须是稳定且唯一的(不可使用数组索引代替)- 任何与 UI 相关的状态都不应放在数据模型中
- 数据模型只描述业务数据本身
3.2 状态变量设计
ArkTS 的 @State 装饰器用于声明组件的响应式状态变量。当 @State 修饰的变量发生变化时,框架会自动重新渲染关联的 UI:
@State private todoList: TodoItem[] = [ /* 初始数据 */ ];
@State private selectedIds: Set<number> = new Set();
@State private isSelectingMode: boolean = false;
@State private isAllSelected: boolean = false;
| 状态变量 | 类型 | 用途 |
|---|---|---|
todoList |
TodoItem[] |
列表数据源,增删数据直接修改此数组 |
selectedIds |
Set<number> |
存储选中项的 ID,高效判定和操作 |
isSelectingMode |
boolean |
控制选择模式 UI 的显示/隐藏 |
isAllSelected |
boolean |
全选按钮的状态指示 |
Set 作为选中状态容器的优势
传统做法可能使用 boolean[] 或 number[] 来追踪选中状态。我们选用 Set<number> 的原因:
- O(1) 查找性能:
set.has(id)比array.includes(id)高效得多 - 天然去重:同一条数据不会出现重复选中
- 简洁的增删操作:
add()/delete()语义清晰 - 增量更新友好:只需传递状态变化而非全量快照
@State 与引用类型
Set 是引用类型。直接调用 set.add() / set.delete() 不会改变 Set 对象的引用,因此 @State 无法检测到变化。解决方案是每次修改后创建一个新引用:
this.selectedIds.add(id);
this.selectedIds = new Set(this.selectedIds); // 重新赋值,触发 UI 刷新
四、布局实现详解
4.1 整体页面结构
页面采用三层结构设计,从上到下依次为:
Column(整体容器)
├── 顶部操作栏(Row)
│ ├── 标题文本
│ └── 操作按钮(选择/全选/删除)
├── 统计信息行(Row)
│ └── 列表总数 / 已选数量
└── 列表主体(List)
├── ListItem × N(ForEach 渲染)
│ ├── Checkbox
│ └── 文字内容区
│ ├── 标题
│ └── 时间 + 标签
这种分层结构的优势:
- 关注点分离:每层只负责自己的展示和交互逻辑
- 易于维护:通过
@Builder将每层拆分为独立的构建函数 - 响应式友好:状态变化逐层传递,不会产生级联重排
4.2 顶部操作栏
操作栏在「普通模式」和「选择模式」下显示不同的内容:
普通模式:
Row() {
Text('待办事项') // 标题
.layoutWeight(1) // 占满剩余空间
Button('选择') // 进入选择模式
}
选择模式:
Row() {
Text('选择事项') // 标题
.layoutWeight(1)
Button('全选/取消全选')
Button('删除')
.enabled(selectedIds.size > 0) // 动态控制禁用态
}
关键细节:
layoutWeight(1)让标题占据水平方向的剩余空间,确保按钮右对齐- 删除按钮的
enabled属性与opacity联动,无选中时视觉上灰化且不可点击 opacity手动控制透明度(0.4),配合enabled(false)提供更清晰的视觉反馈
4.3 列表项布局
每个列表项是一个水平 Row 布局,包含 Checkbox 和文字信息两大部分:
ListItem() {
Row() {
Checkbox()
.select(selectedIds.has(item.id))
.shape(CheckBoxShape.ROUNDED_SQUARE)
.selectedColor('#007AFF')
.onChange((value) => updateSelection(item.id, value))
Column() {
Text(item.title)
.maxLines(1)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Row() {
Text(item.time)
Text(item.tag)
.borderRadius(4)
.backgroundColor('#E8F0FE')
}
}
.layoutWeight(1)
.margin({ left: 8 })
}
}
.selectable(true)
.onSelect((isSelected) => updateSelection(item.id, isSelected))
布局细节:
- 使用
Column内嵌 Row 实现标题+底部信息的多行布局 layoutWeight(1)使文字区域自动填满剩余空间- 标签使用
.borderRadius+.backgroundColor模拟徽章效果 maxLines(1)+textOverflow(Ellipsis)防止超长标题溢出
4.4 事件处理的双通道设计
多选列表的事件处理存在两条路径:
路径 A:点击 ListItem 区域
└─→ ListItem.onSelect(isSelected: boolean)
└─→ updateSelection(item.id, isSelected)
路径 B:点击 Checkbox 组件
└─→ Checkbox.onChange(value: boolean)
└─→ updateSelection(item.id, value)
两条路径最终汇入同一个 updateSelection 方法,确保状态的一致性。
为什么需要两条路径?
- 路径 A 让用户点击列表项的任意位置都能切换选中状态,提升操作便利性
- 路径 B 提供了精确点击 Checkbox 的方式,符合传统多选交互习惯
- 两条路径相互备份,不会因某一方的行为变化而导致功能失效
五、核心业务逻辑
5.1 选择模式切换
进入选择模式时,需要清空之前可能遗留的选中记录:
enterSelectMode(): void {
this.isSelectingMode = true;
this.selectedIds.clear();
this.isAllSelected = false;
this.selectedIds = new Set(this.selectedIds); // 触发 @State 刷新
}
退出选择模式有两条途径:
- 用户点击"完成"按钮(示例中未显式提供,可通过添加"取消"按钮实现)
- 删除所有列表项后自动退出
5.2 选中状态同步
updateSelection 方法是整个多选逻辑的中枢:
updateSelection(id: number, value: boolean): void {
if (value) {
this.selectedIds.add(id);
} else {
this.selectedIds.delete(id);
}
// 创建新 Set 引用以触发 @State 响应式更新
this.selectedIds = new Set(this.selectedIds);
// 更新全选状态
this.isAllSelected = this.selectedIds.size === this.todoList.length;
}
注意事项:
- 始终在修改后重新赋值
selectedIds,否则 UI 不会刷新 isAllSelected的计算依赖于每一次变化,不能滞后- 该方法同时被
onSelect和Checkbox.onChange调用
5.3 全选/取消全选
toggleSelectAll(): void {
if (this.isAllSelected) {
// 取消全选
this.selectedIds.clear();
this.isAllSelected = false;
} else {
// 全选
this.todoList.forEach(item => {
this.selectedIds.add(item.id);
});
this.isAllSelected = true;
}
this.selectedIds = new Set(this.selectedIds);
}
设计要点:
- 使用
todoList.forEach遍历全部 items,而不是直接操作 Set - 全选/取消全选是互斥操作,通过
isAllSelected分支 - 修改后务必重新赋值触发刷新
5.4 批量删除
删除操作包含前置确认环节:
deleteSelectedItems(): void {
if (this.selectedIds.size === 0) return;
promptAction.showDialog({
title: '确认删除',
message: `确定要删除选中的 ${this.selectedIds.size} 项吗?`,
buttons: [
{ text: '取消', color: '#666666' },
{ text: '删除', color: '#FF3B30' }
]
}).then((result) => {
if (result.index === 1) {
this.doDelete();
}
});
}
doDelete(): void {
this.todoList = this.todoList.filter(
item => !this.selectedIds.has(item.id)
);
this.selectedIds.clear();
this.isAllSelected = false;
this.selectedIds = new Set(this.selectedIds);
if (this.todoList.length === 0) {
this.isSelectingMode = false;
}
}
弹窗风格建议:
- 「取消」按钮使用中性色(
#666666) - 「删除」等确认操作按钮使用警示色(
#FF3B30红色) - 按钮顺序遵循平台惯例:取消在左,确认在右
删除后的清理工作:
- 从
todoList中过滤掉已选中的项 - 清空
selectedIds集合 - 重置全选状态
- 列表为空时自动退出选择模式
六、性能优化指南
6.1 keyGenerator 的重要性
ForEach 的 keyGenerator 参数直接影响列表的 diff 复用效率。错误的 key 会导致不必要的节点重建甚至渲染异常。
最佳实践:
// ✅ 使用数据实体的唯一 ID
ForEach(arr, item => { ... }, item => item.id.toString())
// ❌ 使用数组索引(会导致列表项复用混乱)
ForEach(arr, item => { ... }, (_, idx) => idx.toString())
使用索引作为 key 的问题在于:当列表发生插入/删除时,后续项的索引会变化,框架无法正确识别哪些是新增、哪些是已有项。
6.2 @State 更新的边界控制
每次 @State 变量的赋值都会触发组件及其子组件的重新渲染。频繁更新会导致性能问题。
优化策略:
-
批量更新:将多次操作合并为一次赋值
// ❌ 不推荐:多次触发重渲染 this.selectedIds.add(id1); this.selectedIds = new Set(this.selectedIds); this.selectedIds.add(id2); this.selectedIds = new Set(this.selectedIds); // ✅ 推荐:一次性修改,一次触发 this.selectedIds.add(id1); this.selectedIds.add(id2); this.selectedIds = new Set(this.selectedIds); -
大数据量建议:对于包含大量列表项的场景,建议使用
LazyForEach替代ForEach。LazyForEach只在需要时才创建 UI 节点,内存占用更低。
6.3 List 的复用机制
List 组件内部实现了 ListItem 的节点复用。当某个列表项滚出视口时,其对应的 UI 节点会被回收并重新分配给即将滚入视口的新数据项。这个过程是自动的,但需要满足以下条件才能获得最佳复用效果:
- 所有
ListItem的布局结构相同 - key 值稳定且在每次渲染中保持一致
- 避免在
ListItem内部使用条件渲染导致结构变化
6.4 使用 LazyForEach 处理大数据集
当列表数据超过 100 条时,建议使用 LazyForEach:
class TodoDataSource implements IDataSource {
private data: TodoItem[] = [];
totalCount(): number {
return this.data.length;
}
getData(index: number): TodoItem {
return this.data[index];
}
// ... 其他接口实现
}
// 在 build 中使用
List() {
LazyForEach(this.dataSource, (item: TodoItem) => {
ListItem() { /* ... */ }
}, (item: TodoItem) => item.id.toString())
}
LazyForEach 只有在项即将进入视口时才会创建对应的 UI 节点,显著减少了启动时和滚动时的内存消耗。
七、常见问题与解决方案
7.1 状态不同步:Checkbox 点击和 ListItem.onSelect 冲突
现象:点击 Checkbox 后,选择状态变化了两倍(选中→取消→选中)或完全反向。
原因:点击 Checkbox 同时触发了 Checkbox.onChange 和 ListItem.onSelect 两条路径,两者互相覆盖。
解决方案:
- 统一在
updateSelection方法中做幂等处理:Set.add()对已经存在的元素不会重复添加 - 确保
onSelect和onChange调用的目标方法一致 - 在
onSelect回调中不依赖isSelected的当前值,而是使用传入的参数
7.2 UI 不刷新:Set 修改后视图无变化
现象:执行了 selectedIds.add(1) 后视图没有任何变化。
原因:Set 是引用类型,调用 add() / delete() 不会改变引用地址,@State 无法检测到变化。
解决方案:每次修改后重新赋值:
this.selectedIds.add(id);
this.selectedIds = new Set(this.selectedIds); // 必须重新赋值
7.3 列表项无法滚动
现象:List 容器内的内容不能滚动。
原因:List 的高度/宽度没有明确指定,或者被父容器裁剪。
解决方案:确保 List 的高度为确定值或 100%:
List() { /* ... */ }
.width('100%')
.height('100%') // 必须设置明确高度
7.4 删除操作后列表渲染异常
现象:删除某些项后,列表中出现空白项或数据错位。
原因:ForEach 的 keyGenerator 使用了数组索引而非唯一 ID,导致删除后复用索引指向了错误的数据。
解决方案:使用数据实体的唯一标识作为 key,而非索引。
7.5 全选/取消全选与手动选择的状态不一致
现象:全选后手动取消某一项,全选按钮仍然显示为「全选」状态。
原因:isAllSelected 的更新逻辑不完整。
解决方案:在每一次选中状态变化时重新计算 isAllSelected:
updateSelection(id: number, value: boolean): void {
// ...
this.isAllSelected = this.selectedIds.size === this.todoList.length;
}
八、扩展与变体
8.1 单选模式
将 Set<number> 替换为单个 number 变量,每次选择时覆盖之前的选中项:
@State private selectedId: number = -1;
updateSelection(id: number): void {
this.selectedId = id; // 每次只保留一个选中项
}
8.2 区间选择(Shift 多选)
通过记录最后一次点击的索引,结合 Shift 键判定实现区间选择:
@State private lastSelectedIndex: number = -1;
handleSelect(index: number, isShift: boolean): void {
if (isShift && this.lastSelectedIndex >= 0) {
const start = Math.min(this.lastSelectedIndex, index);
const end = Math.max(this.lastSelectedIndex, index);
for (let i = start; i <= end; i++) {
this.selectedIds.add(this.todoList[i].id);
}
} else {
this.selectedIds.add(this.todoList[index].id);
this.lastSelectedIndex = index;
}
this.selectedIds = new Set(this.selectedIds);
}
8.3 滑动删除(SwipeDelete)
HarmonyOS NEXT 提供了 SwipeAction 组件,可与多选列表组合:
ListItem() {
SwipeAction() {
// 侧滑时显示的删除按钮
Button('删除')
.onClick(() => this.deleteItem(item.id))
}
// 默认内容
Row() {
Checkbox()
// ...
}
}
8.4 拖拽排序
结合 List 的 .onItemDragStart() / .onItemDrop() 可实现拖拽排序:
List() {
ForEach(this.todoList, (item) => {
ListItem() { /* ... */ }
.onDragStart(() => item.id)
})
}
.onItemDrop((event, extraParams) => {
// 处理拖拽释放后的排序逻辑
})
8.5 空状态占位
当列表数据为空时,显示友好的空状态提示:
if (this.todoList.length === 0) {
Column() {
Image($r('app.media.empty_icon'))
.width(120)
.height(120)
Text('暂无待办事项')
.fontSize(16)
.fontColor('#999999')
.margin({ top: 16 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
} else {
this.buildTodoList()
}
8.6 搜索过滤
结合搜索框对列表进行过滤:
@State private searchText: string = '';
@State private filteredList: TodoItem[] = this.todoList;
get filteredList() {
return this.todoList.filter(item =>
item.title.includes(this.searchText)
);
}
九、最佳实践总结
9.1 架构层面
- 状态管理集中化:所有选中相关的逻辑收敛到
updateSelection一个方法中,避免分散导致的不一致 - 数据驱动 UI:不要尝试直接操作 DOM/UI 节点,所有变化都通过
@State变量驱动 - 单向数据流:数据从
@State流向 UI,事件从 UI 回调到处理方法,形成闭环
9.2 交互设计层面
- 确认弹窗:批量删除等不可逆操作必须有确认步骤
- 禁用态反馈:无选中项时按钮应灰化(
enabled+opacity双重保障) - 实时反馈:选中数量、全选状态等统计信息应随用户操作即时更新
- 动态按钮标签:全选/取消全选的按钮文字随状态变化,降低用户认知负担
9.3 代码质量层面
- 使用 @Builder 拆分:将复杂的 UI 拆分为多个
@Builder方法,提升可读性和复用性 - Key 必填:
ForEach一定提供keyGenerator - 注释完整:关键属性和业务逻辑旁添加中文注释
- 类型明确:使用接口(
interface)而非匿名对象,提供类型安全
9.4 性能层面
- 超过 100 条数据使用
LazyForEach - 避免在
ForEach内部做耗时计算 - 批量修改状态时集中赋值,减少重渲染次数
List的高度必须明确,避免动态计算导致滚动失效
十一、Compose 与开发调试技巧
11.1 @Builder 与 @BuilderParam 的复用策略
在复杂的多选列表场景中,合理运用 @Builder 可以显著提升代码的可维护性。除了将页面拆分为多个 @Builder 方法外,还可以通过 @BuilderParam 实现模板化插槽:
@Component
struct ListItemTemplate {
@BuilderParam content: () => void;
build() {
ListItem() {
this.content()
}
.selectable(true)
}
}
这样,业务页面可以通过传入不同的 @Builder 来复用同一套选中交互逻辑。
11.2 @Watch 监听状态变化
当需要监听某个 @State 变量的变化并执行副作用(如本地缓存选中状态)时,可以使用 @Watch 装饰器:
@State @Watch('onSelectionChanged') private selectedIds: Set<number> = new Set();
onSelectionChanged(): void {
// 将选中状态持久化到本地存储
AppStorage.set('savedSelection', Array.from(this.selectedIds));
}
11.3 使用 DevEco Studio 的 Inspector 调试布局
DevEco Studio 提供了强大的 UI Inspector 工具,用于调试 List 布局:
- 运行应用后,点击 DevEco Studio 底部面板的「Inspector」标签
- 在设备/模拟器上选中任意 UI 元素,Inspector 会自动定位到对应的组件树
- 可以直接在 Inspector 中查看和修改组件属性,实时观察效果
- 对于 List 组件,可以重点关注:
selectable属性是否正确设置、ListItem的onSelect回调是否绑定成功
11.4 日志辅助调试
在开发阶段,通过打印选中状态变化可以帮助定位交互问题:
updateSelection(id: number, value: boolean): void {
console.info(`[Selection] item ${id}: ${value ? 'selected' : 'deselected'}`);
console.info(`[Selection] current size: ${this.selectedIds.size}`);
// ...
}
在 HarmonyOS NEXT 中,推荐使用 console.info / console.warn / console.error 进行分类输出。日志可以在 DevEco Studio 的 Log 面板中查看。
十二、多选列表的测试策略
12.1 单元测试(UT)
针对核心业务逻辑编写单元测试:
// 测试 updateSelection 方法
describe('updateSelection', () => {
it('should add id when selected', () => {
const demo = new ListMultipleSelectDemo();
demo.updateSelection(1, true);
expect(demo.selectedIds.has(1)).toBe(true);
});
it('should remove id when deselected', () => {
const demo = new ListMultipleSelectDemo();
demo.updateSelection(1, true);
demo.updateSelection(1, false);
expect(demo.selectedIds.has(1)).toBe(false);
});
it('should update isAllSelected correctly', () => {
const demo = new ListMultipleSelectDemo();
// 全选
demo.todoList.forEach(item => demo.updateSelection(item.id, true));
expect(demo.isAllSelected).toBe(true);
});
});
12.2 UI 自动化测试
使用 @ohos.uitest 框架可以编写 UI 层面的自动化测试,模拟用户操作来验证多选列表的功能完整性:
import { Driver, ON, Component } from '@ohos.uitest';
async function testMultipleSelect() {
const driver = await Driver.create();
// 点击「选择」按钮进入选择模式
const selectBtn = await driver.findComponent(ON.text('选择'));
await selectBtn.click();
// 点击第一项
const firstItem = await driver.findComponent(ON.text('完成项目需求文档'));
await firstItem.click();
// 验证选中数量显示
const statsText = await driver.findComponent(ON.text('已选 1 项'));
expect(await statsText.isExist()).toBe(true);
}
12.3 边界条件测试清单
在提交代码前,建议逐一验证以下边界条件:
| 测试场景 | 期望结果 |
|---|---|
| 空列表时点击「选择」 | 操作栏正常切换,无崩溃 |
| 全选全部 8 项 | 统计显示「已选 8 项」 |
| 全选后逐项取消 | 全选按钮文字变为「全选」 |
| 快速连续点击 Checkbox | 状态稳定,不发生抖动 |
| 删除所有项 | 列表为空,自动退出选择模式 |
| 删除过程中断(取消弹窗) | 列表不变,选中状态保持 |
| 未选中时点击「删除」 | 按钮禁用,弹窗不弹出 |
| 列表滚动到底部后回顶部 | 选中状态不丢失 |
| 页面跳转返回 | 状态可恢复(或需配合持久化) |
十三、与其他布局方式的组合
13.1 List + Badge(角标)
在列表项右侧添加数字角标,标识未读消息数量:
Badge({
count: item.unreadCount,
position: BadgePosition.RightTop,
style: { badgeSize: 16, fontSize: 10 }
}) {
Text(item.title)
}
将 Badge 与多选列表组合,常见于邮件应用或消息应用。
13.2 List + SwipeAction(侧滑菜单)
侧滑操作与多选功能互补,提供快速单条操作的入口:
ListItem() {
SwipeAction({ end: this.deleteButton }) {
Row() {
Checkbox()
// ... 内容
}
}
}
.selectable(true)
设计原则:侧滑适用于「快速单条操作」,多选适用于「批量操作」,两者不应互斥。
13.3 List + Refresh(下拉刷新)
结合 RefreshComponent 实现列表的下拉刷新:
Refresh({ refreshing: $$this.isRefreshing }) {
List() {
// ... 列表内容
}
}
.onRefresh(() => {
// 重新加载数据
this.loadData();
})
在刷新完成后,需要保持或重置选中状态,取决于业务需求:
- 常规做法:刷新后清空选中状态
- 增量更新:刷新后尝试恢复之前的选中状态
13.4 List + Search(搜索过滤)
集成搜索功能后,多选列表的逻辑需要做相应调整:
@State private searchText: string = '';
get displayList(): TodoItem[] {
if (this.searchText.trim() === '') {
return this.todoList;
}
return this.todoList.filter(item =>
item.title.includes(this.searchText) ||
item.tag.includes(this.searchText)
);
}
重要注意事项:
当搜索过滤生效时,用户看到的列表是 displayList(过滤后),但全选操作应对原数据源 todoList 还是过滤后的列表?这取决于产品需求:
- 方案 A(推荐):全选仅针对过滤后的列表,例如「全选当前搜索结果」
- 方案 B:全选针对全部数据,适合管理后台批量操作
toggleSelectAll(): void {
const targetList = this.searchText ? this.displayList : this.todoList;
if (this.isAllSelected) {
targetList.forEach(item => this.selectedIds.delete(item.id));
this.isAllSelected = false;
} else {
targetList.forEach(item => this.selectedIds.add(item.id));
this.isAllSelected = true;
}
this.selectedIds = new Set(this.selectedIds);
}
十四、无障碍访问(Accessibility)
14.1 为多选列表添加无障碍标签
HarmonyOS NEXT 提供了无障碍支持 API,让视觉障碍用户也能顺畅使用多选功能:
ListItem() {
// ...
}
.selectable(true)
.accessibilityGroup(true) // 标记为无障碍焦点组
.accessibilityText(`待办事项:${item.title},${this.selectedIds.has(item.id) ? '已选中' : '未选中'}`) // 读屏内容
14.2 动态更新无障碍描述
当选中状态变化时,同步更新无障碍描述:
updateSelection(id: number, value: boolean): void {
// ...
// 更新读屏焦点
this.getUIContext()?.runScA11yTask(() => {
// 通知无障碍服务选中状态已变化
});
}
14.3 触觉反馈
在开关选择模式或执行批量操作时,可触发触觉反馈增强交互确认感:
import { vibrator } from '@kit.SensorServiceKit';
enterSelectMode(): void {
// ...
try {
vibrator.startVibration({
type: 'preset',
effectId: 'haptic.armature.trigger',
count: 1
}, (err) => { /* 处理错误 */ });
} catch (err) {
console.error('振动反馈失败:', err);
}
}
十五、国际化与多语言适配
15.1 字符串资源抽取
将 UI 中的中文文本抽取到资源文件中,方便后续国际化:
{
"string": [
{ "name": "list_title_normal", "value": "待办事项" },
{ "name": "list_title_selecting", "value": "选择事项" },
{ "name": "btn_select", "value": "选择" },
{ "name": "btn_select_all", "value": "全选" },
{ "name": "btn_deselect_all", "value": "取消全选" },
{ "name": "btn_delete", "value": "删除" },
{ "name": "delete_confirm_title", "value": "确认删除" },
{ "name": "delete_confirm_message", "value": "确定要删除选中的 %d 项吗?此操作不可撤销。" },
{ "name": "selected_count", "value": "已选 %d 项" },
{ "name": "total_count", "value": "共 %d 项" }
]
}
15.2 使用 $r 引用资源
在代码中通过 $r 引用多语言资源:
Text($r('app.string.list_title_normal'))
Button($r('app.string.btn_select'))
这样当应用切换到不同语言区域时,界面文本会自动切换,无需修改代码。
十六、与后端数据同步
16.1 批量删除的网络请求
在实际应用中,批量删除通常需要与后端 API 交互:
async doDelete(): Promise<void> {
const idsToDelete = Array.from(this.selectedIds);
try {
// 显示加载状态
this.isLoading = true;
// 发起批量删除请求
const response = await http.request({
method: http.RequestMethod.DELETE,
url: 'https://api.example.com/todos/batch',
header: { 'Content-Type': 'application/json' },
extraData: JSON.stringify({ ids: idsToDelete })
});
if (response.responseCode === 200) {
// 后端删除成功后,更新本地数据
this.todoList = this.todoList.filter(
item => !this.selectedIds.has(item.id)
);
// ... 后续清理
} else {
// 处理失败情况
promptAction.showDialog({ title: '删除失败', message: response.result });
}
} catch (err) {
console.error('网络请求失败:', JSON.stringify(err));
promptAction.showDialog({ title: '网络错误', message: '请检查网络连接后重试' });
} finally {
this.isLoading = false;
}
}
16.2 乐观更新(Optimistic Update)
对于需要快速响应的场景,可以先更新本地 UI(乐观更新),再异步同步后端:
deleteSelectedItems(): void {
// 1. 先保存待删除的 ID 快照
const snapshot = new Set(this.selectedIds);
// 2. 乐观更新:立即更新 UI
this.todoList = this.todoList.filter(item => !snapshot.has(item.id));
this.selectedIds.clear();
this.selectedIds = new Set(this.selectedIds);
// 3. 异步同步后端
this.syncDeleteToServer(snapshot).catch(() => {
// 同步失败时回滚
this.todoList = [...this.todoList, ...this.rollbackData];
promptAction.showDialog({ title: '同步失败', message: '服务器同步失败,但本地已删除' });
});
}
乐观更新可以显著提升用户体验,让批量操作感觉几乎没有延迟。但需要做好失败回滚策略。
十七、版本适配与兼容性
17.1 API 版本兼容
本文示例基于 HarmonyOS NEXT(API 12+)开发。不同 API 版本对 List 和 Checkbox 组件的支持存在差异:
| API 版本 | selectable | onSelect | CheckBoxShape | 推荐度 |
|---|---|---|---|---|
| API 9 | 不支持 | 不支持 | 仅 CIRCLE | ❌ |
| API 10 | 支持 | 支持 | CIRCLE / ROUNDED_SQUARE | ⚠️ |
| API 11 | 支持 | 支持 | 完整支持 | ✅ |
| API 12+ | 支持 | 支持 | 完整支持 + 新特性 | ✅✅ |
如果项目需要兼容 API 10/11,需要注意:
CheckBoxShape.ROUNDED_SQUARE在较低版本中可能不可用,可回退到CheckBoxShape.CIRCLE@Watch装饰器在 API 11 中行为有所调整LazyForEach在 API 12+ 中性能有大幅优化
17.2 渐进增强策略
if (canIUse('ListItem.selectable')) {
// 使用原生 selectable 属性
} else {
// 降级方案:手动管理选中状态 + onClick
}
HarmonyOS NEXT 提供了 canIUse API 用于检测当前系统是否支持特定特性,便于实现渐进增强。
十八、结语
「List + multipleSelect 多选列表」是鸿蒙 ArkTS 开发中最常用也最实用的布局模式之一。本文从一个完整的待办事项多选删除示例出发,系统地剖析了其背后的组件体系(List / ListItem / Checkbox)、状态管理(@State / Set)、事件处理(onSelect / onChange)以及业务逻辑(全选/反选/批量删除)的全链路实现。
从最初的核心概念,到性能优化、无障碍访问、国际化适配、后端同步乃至版本兼容,本文力图覆盖多选列表开发全生命周期中所涉及的所有关键环节。读者可以根据自己的项目阶段和需求,选择性阅读和实践其中对应的知识点。
掌握这一布局模式不仅能够应对日常开发中的多选交互需求,更能加深对鸿蒙 ArkTS 响应式框架的理解——尤其是组件生命周期、状态驱动刷新和事件分发机制等核心概念。希望读者在理解本文内容的基础上,能够举一反三,灵活运用 List 组件构建出更复杂、更流畅、更优雅的用户界面。
而学习的最佳方式就是动手实践。建议读者打开 DevEco Studio,创建示例工程,按照本文的代码一步步搭建自己的多选列表,并在实际运行中验证每一个概念和技巧。唯有如此,才能真正将知识转化为能力。
本文对应的完整示例代码位于 entry/src/main/ets/pages/ListMultipleSelectDemo.ets,可在 DevEco Studio 中直接运行体验。
更多推荐



所有评论(0)