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

在这里插入图片描述
在这里插入图片描述

一、概述

在鸿蒙 HarmonyOS NEXT(API 12+)应用开发中,列表(List)是最常见、最重要的 UI 容器之一。无论是社交应用的消息流、电商应用的商品列表,还是办公应用的任务看板,几乎所有涉及数据集合展示的场景都离不开列表组件。而当列表需要与用户进行批量交互——例如多选删除、批量标记、批量移动——时,「List + multipleSelect 多选列表」布局模式便成为必不可少的解决方案。

本文将以一个完整的「待办事项多选删除」示例应用为主线,深入剖析鸿蒙原生 ArkTS 框架中 ListListItemCheckbox 以及 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 组件

ListItemList 的直接子组件,代表列表中的单个条目。它是多选交互的核心载体。

2.2.1 selectable 属性

selectableListItem 独有的布尔属性,控制该列表项是否参与选中交互:

ListItem()
  .selectable(true)   // 启用选中功能

selectable(true) 时,ListItem 会:

  • 监听用户的点击/触摸事件
  • 维护内部的选中状态开关
  • 在状态变化时触发 onSelect 回调

selectable(false)(默认值)时,ListItem 忽略选中逻辑,点击事件直接透传到其内部的子组件。

2.2.2 onSelect 回调

onSelectListItem 提供的事件回调,当用户触摸某个开启了 selectable 的列表项时触发:

ListItem()
  .selectable(true)
  .onSelect((isSelected: boolean) => {
    // isSelected: true → 选中, false → 取消选中
  })

重要行为特征

  • onSelect 在每次选中/取消选中时都会触发
  • 回调携带的 isSelected 参数是 ListItem 内部维护的选中状态
  • 点击 ListItem 的任何区域都会触发(不仅仅是 Checkbox)
  • onSelectonClick 可能同时触发,两者不互斥

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 // 键值生成器(用于列表复用优化)
)

在多选列表中,ForEachkeyGenerator 尤为关键。它返回每个列表项的唯一标识符,帮助框架在数据变化时精确识别哪些项需要添加、移除或更新。通常使用数据实体的 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> 的原因:

  1. O(1) 查找性能set.has(id)array.includes(id) 高效得多
  2. 天然去重:同一条数据不会出现重复选中
  3. 简洁的增删操作add() / delete() 语义清晰
  4. 增量更新友好:只需传递状态变化而非全量快照
@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 刷新
}

退出选择模式有两条途径:

  1. 用户点击"完成"按钮(示例中未显式提供,可通过添加"取消"按钮实现)
  2. 删除所有列表项后自动退出

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 的计算依赖于每一次变化,不能滞后
  • 该方法同时被 onSelectCheckbox.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 红色)
  • 按钮顺序遵循平台惯例:取消在左,确认在右

删除后的清理工作

  1. todoList 中过滤掉已选中的项
  2. 清空 selectedIds 集合
  3. 重置全选状态
  4. 列表为空时自动退出选择模式

六、性能优化指南

6.1 keyGenerator 的重要性

ForEachkeyGenerator 参数直接影响列表的 diff 复用效率。错误的 key 会导致不必要的节点重建甚至渲染异常。

最佳实践

// ✅ 使用数据实体的唯一 ID
ForEach(arr, item => { ... }, item => item.id.toString())

// ❌ 使用数组索引(会导致列表项复用混乱)
ForEach(arr, item => { ... }, (_, idx) => idx.toString())

使用索引作为 key 的问题在于:当列表发生插入/删除时,后续项的索引会变化,框架无法正确识别哪些是新增、哪些是已有项。

6.2 @State 更新的边界控制

每次 @State 变量的赋值都会触发组件及其子组件的重新渲染。频繁更新会导致性能问题。

优化策略

  1. 批量更新:将多次操作合并为一次赋值

    // ❌ 不推荐:多次触发重渲染
    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);
    
  2. 大数据量建议:对于包含大量列表项的场景,建议使用 LazyForEach 替代 ForEachLazyForEach 只在需要时才创建 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.onChangeListItem.onSelect 两条路径,两者互相覆盖。

解决方案

  1. 统一在 updateSelection 方法中做幂等处理:Set.add() 对已经存在的元素不会重复添加
  2. 确保 onSelectonChange 调用的目标方法一致
  3. 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 删除操作后列表渲染异常

现象:删除某些项后,列表中出现空白项或数据错位。

原因ForEachkeyGenerator 使用了数组索引而非唯一 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 架构层面

  1. 状态管理集中化:所有选中相关的逻辑收敛到 updateSelection 一个方法中,避免分散导致的不一致
  2. 数据驱动 UI:不要尝试直接操作 DOM/UI 节点,所有变化都通过 @State 变量驱动
  3. 单向数据流:数据从 @State 流向 UI,事件从 UI 回调到处理方法,形成闭环

9.2 交互设计层面

  1. 确认弹窗:批量删除等不可逆操作必须有确认步骤
  2. 禁用态反馈:无选中项时按钮应灰化(enabled + opacity 双重保障)
  3. 实时反馈:选中数量、全选状态等统计信息应随用户操作即时更新
  4. 动态按钮标签:全选/取消全选的按钮文字随状态变化,降低用户认知负担

9.3 代码质量层面

  1. 使用 @Builder 拆分:将复杂的 UI 拆分为多个 @Builder 方法,提升可读性和复用性
  2. Key 必填ForEach 一定提供 keyGenerator
  3. 注释完整:关键属性和业务逻辑旁添加中文注释
  4. 类型明确:使用接口(interface)而非匿名对象,提供类型安全

9.4 性能层面

  1. 超过 100 条数据使用 LazyForEach
  2. 避免在 ForEach 内部做耗时计算
  3. 批量修改状态时集中赋值,减少重渲染次数
  4. 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 布局:

  1. 运行应用后,点击 DevEco Studio 底部面板的「Inspector」标签
  2. 在设备/模拟器上选中任意 UI 元素,Inspector 会自动定位到对应的组件树
  3. 可以直接在 Inspector 中查看和修改组件属性,实时观察效果
  4. 对于 List 组件,可以重点关注:selectable 属性是否正确设置、ListItemonSelect 回调是否绑定成功

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 版本对 ListCheckbox 组件的支持存在差异:

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 中直接运行体验。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐