鸿蒙 ArkUI 多选列表实现详解:从 Flutter CheckboxListTile 到 HarmonyOS 的完整迁移指南
鸿蒙 ArkUI 多选列表实现详解:从 Flutter CheckboxListTile 到 HarmonyOS 的完整迁移指南
一、引言




在移动应用开发中,多选列表(Multi-select List) 是最常见也最实用的交互模式之一。无论是购物 App 的购物车商品选择、文件管理器的批量操作、聊天记录的批量删除,还是任务管理中的多任务标记完成,都离不开这一基础 UI 模式。
对于熟悉 Flutter 的开发者来说,CheckboxListTile 是一个再熟悉不过的组件——它优雅地将复选框与列表项融为一体,通过 onChanged 回调实现选中状态的实时更新,配合 ListView.builder 高效构建长列表,用 Row、Expanded、Container、Padding 等布局组件完成视觉呈现。
然而,当项目迁移到 HarmonyOS(鸿蒙) 平台时,Flutter 组件并不能直接使用。鸿蒙有着自己独立的 UI 框架——ArkUI,它使用 ArkTS(方舟 TypeScript) 作为开发语言,提供了一套强大的声明式 UI 组件体系。
本文将以一个购物清单多选列表为实战案例,逐行解析如何用 ArkUI 的 List、ForEach、ListItem、Checkbox、Row、Column、Flex、Blank、Button 等组件,构建出与 Flutter CheckboxListTile + ListView.builder 完全等效的多选列表。同时,本文将深入探讨 ArkTS 的状态管理机制、Previewer 兼容性技巧、以及从 Flutter 到 ArkUI 的组件映射方法论。
本文配套的完整源码位于 entry/src/main/ets/pages/Index.ets,你可以直接在 DevEco Studio 的 Previewer 中预览效果。
二、项目背景与技术选型
2.1 为什么需要多选列表?
多选列表是现代移动应用中最核心的批量操作入口。以一个典型的购物 App 为例:
- 勾选商品:用户逐一选择需要购买的商品
- 全选/反选:一键切换所有商品的选中状态
- 实时统计:底部栏动态显示已选数量与总金额
- 批量操作:删除选中商品、批量加入购物车、结算等
这些交互需求构成了多选列表的核心功能矩阵。本案例的购物清单 Demo 正好覆盖了以上所有场景,因此是一个极具代表性的教学案例。
2.2 Flutter 与 ArkUI 的设计哲学对比
Flutter 和 ArkUI 虽然分属不同的技术生态,但在 UI 架构设计上有着惊人的相似之处:
| 设计维度 | Flutter | ArkUI (HarmonyOS) |
|---|---|---|
| 编程语言 | Dart | ArkTS (TypeScript 超集) |
| 声明式 UI | build() 返回 Widget 树 |
build() 返回 Component 树 |
| 状态管理 | setState() + @State |
@State + @Observed + @Link |
| 列表构建 | ListView.builder |
List + ForEach |
| 布局组件 | Row、Column、Expanded、Container |
Row、Column、Flex.layoutWeight、Container |
这种设计上的同源性,使得 Flutter 开发者可以平滑地迁移到 ArkUI 开发。核心的差异在于组件名称和属性语法,而底层的组合思维(Composition over Inheritance)完全一致。
2.3 本项目的 ArkUI 技术栈
本案例使用了以下 ArkUI 核心组件和技术:
@Entry+@Component:声明页面入口和组件@State:响应式状态管理,驱动 UI 自动刷新List+ForEach+ListItem:高效虚拟列表,对应 Flutter 的ListView.builderCheckbox:复选框组件,对应 Flutter 的CheckboxRow/Column:线性布局,与 Flutter 完全对应Flex.layoutWeight():弹性权重,对应 Flutter 的ExpandedBlank():弹性空白占位,对应 Flutter 的SpacerButton:按钮组件AlertDialog:弹窗组件Container:容器组件,支持背景色、阴影等
三、数据模型设计
3.1 ShopItem 接口
在 ArkTS 中,我们使用 interface 定义商品数据模型。注意一个关键点:interface 必须在 @Component 之前声明,否则 ArkTS 编译器会报 “未定义” 错误。
interface ShopItem {
id: number; // 商品唯一标识
name: string; // 商品名称
price: number; // 单价
quantity: number; // 数量
selected: boolean; // 是否被选中
}
这个接口是整篇文章的数据基石。它定义了多选列表的 “原子数据单元”,包含:
- 展示层需要的数据:
name、price、quantity - 交互层需要的数据:
selected(选中状态) - 业务标识:
id(用于跟踪和操作)
在 Flutter 中,等价的 Dart 代码是:
class ShopItem {
int id;
String name;
double price;
int quantity;
bool selected;
}
3.2 为什么用 interface 而不是 class?
在 ArkTS 中,interface 和 class 都可以定义数据结构。这里选择 interface 的原因:
- 轻量:
interface没有运行时开销,仅用于编译时类型检查 - 不可变性友好:本案例采用新建数组 + 新建对象的方式触发
@State刷新,interface的对象字面量语法更简洁 - Previewer 兼容性:在 DevEco Studio Previewer 中,
interface比class更容易通过编译
如果需要观测嵌套属性的变化,则应使用 @Observed 装饰的 class(后文会展开对比)。
3.3 初始化数据
@State shopItems: ShopItem[] = [
{ id: 1, name: '苹果', price: 6.5, quantity: 2, selected: false },
{ id: 2, name: '香蕉', price: 3.2, quantity: 1, selected: false },
{ id: 3, name: '牛奶', price: 19.9, quantity: 1, selected: false },
{ id: 4, name: '面包', price: 8.8, quantity: 2, selected: false },
{ id: 5, name: '鸡蛋', price: 15, quantity: 1, selected: false },
{ id: 6, name: '可乐', price: 3.5, quantity: 3, selected: false },
{ id: 7, name: '薯片', price: 7.5, quantity: 1, selected: false },
{ id: 8, name: '纸巾', price: 12.9, quantity: 1, selected: false },
];
8 个商品覆盖了不同的价格区间和购买数量,能够充分展示列表渲染效果。
四、响应式状态管理
4.1 @State 装饰器
ArkTS 的 @State 是驱动 UI 刷新的核心机制,与 Flutter 的 setState() + StatefulWidget 在概念上等价。当 @State 修饰的变量发生变化时,框架自动重新调用 build(),仅更新有变化的部分。
本案例中使用了三个 @State 变量:
@State shopItems: ShopItem[] = [ ... ]; // 商品列表
@State selectedCount: number = 0; // 选中件数
@State selectAll: boolean = false; // 全选状态
其中 shopItems 是核心数据源,selectedCount 和 selectAll 是派生状态(Derived State)。在 Flutter 中,派生状态通常通过 getter 实时计算,但在 ArkUI 中,由于 @State 刷新机制的粒度限制,显式维护 selectedCount 和 selectAll 可以获得更好的性能。
4.2 状态更新策略:不可变数组模式
这是本案例最重要的设计决策。在 ArkUI 中,@State 对数组的观测是浅引用比较——只有数组引用发生变化,才会触发 UI 刷新。因此:
✅ 正确做法:每次修改数组时创建新数组
this.shopItems = [...this.shopItems]; // 或 arr = [] + 赋值
❌ 错误做法:直接修改数组元素属性
this.shopItems[0].selected = true; // @State 不会检测到变化!
这就是为什么所有修改操作都遵循 “新建数组 → 逐个构建新对象 → 整体赋值” 的模式。以 toggleItem 为例:
toggleItem(index: number): void {
let arr: ShopItem[] = []; // ① 新建空数组
for (let i: number = 0; i < this.shopItems.length; i++) {
let s: boolean = this.shopItems[i].selected;
if (i === index) { s = !s; } // ② 翻转目标项状态
arr.push({ // ③ 构建全新的对象
id: this.shopItems[i].id,
name: this.shopItems[i].name,
price: this.shopItems[i].price,
quantity: this.shopItems[i].quantity,
selected: s
});
}
this.shopItems = arr; // ④ 新数组赋值 → 触发刷新
this.calcSelected(); // ⑤ 更新派生状态
}
这个模式保证了每次状态变化都能被 @State 正确捕获。虽然代码略长,但语义清晰、可预测性强。
4.3 Flutter 中的等价写法
Flutter 中同样的操作只需要一行:
setState(() => items[index].selected = !items[index].selected);
这是因为 Flutter 的 setState 标记整个 Widget 为 “脏”,然后重建整个 Widget 树并对比差异(通过 canUpdate 判断)。而 ArkUI @State 的观测粒度更细,策略不同。两种方式各有利弊:
- Flutter 方式:编码简洁,但大列表全量重建时性能开销大
- ArkUI 方式:需要显式管理不可变性,但对框架更友好,性能更可预测
五、UI 布局逐层解析
整个页面是一个 垂直三栏结构:顶部标题栏 + 中间列表区域 + 底部操作栏。外层用 Column 包裹,通过 layoutWeight(1) 让列表区域填充剩余空间。
5.1 外层容器
Column() {
// ① 顶部标题
// ② 列表区域(layoutWeight(1) 填充)
// ③ 底部操作栏
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
对比 Flutter:
Column(
children: [
// ① 顶部标题
// ② 列表区域 (Expanded)
// ③ 底部操作栏
],
)
5.2 顶部标题栏(Header)
标题栏由 Row 实现左右布局:左侧显示标题 “购物清单”,右侧显示全选复选框。
Row() {
Text('购物清单')
.fontSize(20)
.fontWeight(FontWeight.Bold)
Blank() // ← Spacer,撑开左右
Row() {
Checkbox()
.select(this.selectAll)
.shape(CheckBoxShape.ROUNDED_SQUARE)
.width(18).height(18)
.onChange(() => { this.toggleSelectAll(); })
Blank().width(4) // ← 固定间距
Text('全选')
.fontSize(14)
.fontColor('#666666')
}
.alignItems(VerticalAlign.Center)
.onClick(() => { this.toggleSelectAll(); })
}
这里涉及几个关键点:
-
Blank():这是 ArkUI 的弹性空白组件,等价于 Flutter 的Spacer()。它会占据 Row 中所有剩余空间,将左右两部分推向两端。 -
Blank().width(4):设置最小宽度为 4vp。因为在 Row 中没有其他弹性元素,所以它固定为 4vp,相当于SizedBox(width: 4)。 -
双层点击区域:全选区域同时监听了
Checkbox.onChange和外部Row.onClick,无论用户点击复选框还是点击文字 “全选”,都能触发全选切换。这与 FlutterInkWell包住Row的做法一致。 -
.select():ArkUI 的Checkbox通过.select(boolean)控制选中状态,而非 Flutter 的value参数。
5.3 列表区域(核心)
列表区域是本案例的核心 UI。它使用 List + ForEach + ListItem 的组合,对应 Flutter 的 ListView.builder。
List() {
ForEach(this.shopItems, (item: ShopItem, index?: number) => {
ListItem() {
Row() {
// ← 左侧:复选框
Checkbox()
.select(item.selected)
.shape(CheckBoxShape.ROUNDED_SQUARE)
.width(22).height(22)
.onChange(() => { this.toggleItem(index!); })
// ← 中间:商品信息(Expanded)
Column() {
Text(item.name)
.fontSize(16)
.fontWeight(FontWeight.Medium)
Text('¥' + item.price.toFixed(1) + ' × ' + item.quantity)
.fontSize(13)
.fontColor('#999999')
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.padding({ left: 12, right: 8 })
// ← 右侧:小计金额
Text('¥' + (item.price * item.quantity).toFixed(1))
.fontSize(15)
.fontWeight(FontWeight.Medium)
.fontColor('#FF6B35')
}
.width('100%')
.padding({ left: 16, right: 16, top: 12, bottom: 12 })
.alignItems(VerticalAlign.Center)
}
.backgroundColor(item.selected ? '#F0F7FF' : '#FFFFFF')
.borderRadius(8)
.margin({ top: 5, left: 12, right: 12 })
.onClick(() => { this.toggleItem(index!); })
}, (item: ShopItem) => item.id.toString())
}
5.3.1 Flutter → ArkUI 组件映射表
| Flutter | ArkUI | 说明 |
|---|---|---|
ListView.builder(itemBuilder) |
List { ForEach(...) { ListItem { ... } } } |
列表构建 |
itemCount |
ForEach 遍历数组 |
自动推导数量 |
CheckboxListTile |
ListItem { Row { Checkbox + Column } } |
自定义组合 |
CheckboxListTile.leading |
Row 中的第一个 Checkbox |
左侧装饰 |
CheckboxListTile.title |
Column 中的第一个 Text |
主标题 |
CheckboxListTile.subtitle |
Column 中的第二个 Text |
副标题 |
CheckboxListTile.trailing |
Row 中最后一个 Text |
尾部元素 |
CheckboxListTile.onChanged |
Checkbox.onChange + ListItem.onClick |
状态回调 |
Expanded |
.layoutWeight(1) |
弹性填充 |
EdgeInsets.all/symmetric |
.padding({ left, right, top, bottom }) |
内边距 |
Container |
Container |
容器(名称相同) |
Spacer() |
Blank() |
弹性空白 |
5.3.2 布局结构
每个列表项是一个三列 Row:
┌─────────────┬──────────────────────────────┬────────────┐
│ Checkbox │ Column(商品名称 + 单价×数量) │ ¥小计金额 │
│ (22×22) │ .layoutWeight(1) 弹性填充 │ (右侧对齐) │
└─────────────┴──────────────────────────────┴────────────┘
- 左列:定宽
Checkbox,22×22vp - 中列:
Column垂直排列主标题和副标题,.layoutWeight(1)让它占据 Row 中的剩余空间 - 右列:定宽的小计金额,使用醒目的橙色
#FF6B35
5.3.3 选中态视觉反馈
.backgroundColor(item.selected ? '#F0F7FF' : '#FFFFFF')
当列表项被选中时,背景色从白色 #FFFFFF 切换为浅蓝色 #F0F7FF。这种微妙的颜色变化给用户明确的选中反馈,类似 iOS 系统设置中选中项的交互效果。
5.3.4 点击事件处理
列表项支持两种交互方式:
- 点击复选框:触发
Checkbox.onChange,翻转选中状态 - 点击行区域:触发
ListItem.onClick(相当于 Flutter 的ListTile.onTap),调用toggleItem
两种操作最终调用同一个 toggleItem(index!) 方法,保证行为一致性。这与 Flutter CheckboxListTile 的设计完全一致——点击行任何位置都会触发的 onChanged 回调。
5.3.5 ForEach 的关键参数
ForEach(this.shopItems, (item, index?) => { ... }, (item) => item.id.toString())
- 第一个参数:数据数组
- 第二个参数:构建器函数,接收
item和可选的index - 第三个参数:键生成函数(Key Generator),用于列表 diff 优化
第三个参数对应 Flutter ListView.builder 的 itemBuilder 中的 key 参数。使用 item.id.toString() 作为 key,确保 ArkUI 能正确识别每个列表项的唯一性,在增删操作时只更新变化的部分而不是全量重建。
5.3.6 内联渲染:兼容 Previewer 的关键决策
这里需要特别说明:为什么选择在 ForEach 中直接写 ListItem,而不是用 @Builder 封装?
最初的设计使用了 @Builder itemRow(item: ShopItem) 来封装列表项,但 DevEco Studio Previewer 在编译带有复杂参数类型的 @Builder 时可能会失败(报 “Property ‘space’ does not exist” 等错误)。将所有渲染逻辑直接内联在 ForEach 中,避开了 @Builder 参数传参的编译限制,Previewer 兼容性更好。
5.4 底部操作栏(Bottom Bar)
底部栏固定在页面底部,显示选中统计和操作按钮。
Row() {
Column() {
Text('已选 ' + this.selectedCount + ' 件')
.fontSize(14)
.fontColor('#1A1A1A')
if (this.selectedCount > 0) {
Text('合计 ¥' + this.getSelectedTotal().toFixed(1))
.fontSize(12)
.fontColor('#FF6B35')
}
}
.alignItems(HorizontalAlign.Start) // ← 左对齐
Blank() // ← Spacer
Button('删除选中') ... // ← 删除按钮
Blank().width(12) // ← 固定间距
Button('去结算') ... // ← 结算按钮
}
底部栏是一个三区 Row:
┌──────────┬─────────────────┬──────────┐
│ 统计信息 │ Blank() │ 操作按钮 │
│ (左对齐) │ (弹性空白) │ (右对齐) │
└──────────┴─────────────────┴──────────┘
if (this.selectedCount > 0) 条件渲染合计金额,当没有选中任何商品时,合计金额不显示。这是 ArkUI 条件渲染的典型用法,等价于 Flutter 的 if (count > 0) Text(...)。
按钮的交互状态:
.fontColor(this.selectedCount > 0 ? '#FFFFFF' : '#CCCCCC')
.backgroundColor(this.selectedCount > 0 ? '#FF4D4F' : '#F0F0F0')
.enabled(this.selectedCount > 0)
三个属性联动实现按钮的禁用/启用状态:
- 无选中项时:浅灰背景 + 灰色文字 +
disabled - 有选中项时:品牌色红色/橙色背景 + 白色文字 +
enabled
5.5 弹出确认对话框
Button('去结算') 的 onClick 回调弹出结算确认对话框:
AlertDialog.show({
title: '结算确认',
message: '已选 ' + this.selectedCount + ' 件商品,合计 ¥' + this.getSelectedTotal().toFixed(1),
primaryButton: {
value: '取消',
action: () => {}
},
secondaryButton: {
value: '确认',
action: () => {
this.deleteSelected();
}
}
});
AlertDialog.show() 是 ArkUI 内置的弹窗 API,无需额外引入。它提供 title、message、primaryButton(左按钮)和 secondaryButton(右按钮)四个主要参数。用户点击"确认"后执行结算逻辑(删除已选商品),点击"取消"则关闭弹窗。
六、核心业务逻辑详解
6.1 单选切换:toggleItem
toggleItem(index: number): void {
let arr: ShopItem[] = [];
for (let i: number = 0; i < this.shopItems.length; i++) {
let s: boolean = this.shopItems[i].selected;
if (i === index) { s = !s; }
arr.push({
id: this.shopItems[i].id,
name: this.shopItems[i].name,
price: this.shopItems[i].price,
quantity: this.shopItems[i].quantity,
selected: s
});
}
this.shopItems = arr;
this.calcSelected();
}
这是整个应用最核心的函数。它的工作流程:
- 新建一个空数组
arr - 遍历
this.shopItems,对于每个元素:- 如果是目标索引(
i === index),翻转selected值 - 否则保持原有
selected值 - 创建一个全新的
ShopItem对象推入新数组
- 如果是目标索引(
- 将新数组赋值给
this.shopItems,触发@StateUI 刷新 - 调用
calcSelected()更新派生状态
6.2 全选切换:toggleSelectAll
toggleSelectAll(): void {
let v: boolean = !this.selectAll;
let arr: ShopItem[] = [];
for (let i: number = 0; i < this.shopItems.length; i++) {
arr.push({
id: this.shopItems[i].id,
name: this.shopItems[i].name,
price: this.shopItems[i].price,
quantity: this.shopItems[i].quantity,
selected: v // ← 全部设为同一个值
});
}
this.shopItems = arr;
this.selectedCount = v ? arr.length : 0;
this.selectAll = v;
}
全选逻辑非常直观:v 是当前全选状态的翻转值。如果当前已全选(selectAll = true),则 v = false,全部取消选中;反之全部选中。
这里直接计算 selectedCount 和 selectAll,没有调用 calcSelected(),因为结果已经确定——全选时选中的就是数组长度,取消全选时就是 0。
6.3 选中统计:calcSelected
calcSelected(): void {
let c: number = 0;
for (let i: number = 0; i < this.shopItems.length; i++) {
if (this.shopItems[i].selected) { c++; }
}
this.selectedCount = c;
this.selectAll = (c === this.shopItems.length && this.shopItems.length > 0);
}
单纯的遍历计数。selectAll 的判断条件是:选中数等于总数 且 总数大于 0。后者避免了空列表时 selectAll 为 true 的边界情况。
6.4 批量删除:deleteSelected
deleteSelected(): void {
let arr: ShopItem[] = [];
for (let i: number = 0; i < this.shopItems.length; i++) {
if (!this.shopItems[i].selected) {
arr.push(this.shopItems[i]); // ← 只保留未选中的
}
}
this.shopItems = arr;
this.selectedCount = 0;
this.selectAll = false;
}
"删除"在这里的实现是过滤掉已选中的项。保留所有 selected === false 的项组成新数组。删除后统计信息重置为 0。
在真实项目中,这里可能需要调用后端 API 进行数据库删除操作。本案例关注前端 UI 交互,因此用数据过滤模拟删除效果。
6.5 选中总价计算:getSelectedTotal
getSelectedTotal(): number {
let sum: number = 0;
for (let i: number = 0; i < this.shopItems.length; i++) {
if (this.shopItems[i].selected) {
sum += this.shopItems[i].price * this.shopItems[i].quantity;
}
}
return sum;
}
这是一个 getter(计算属性),在 build() 方法中被调用时实时计算总价。之所以使用 getter 而不是 @State 变量,是因为总价是展示性数据,不需要持久化存储,每次 UI 刷新时重新计算即可。
6.6 生命周期钩子:aboutToAppear
aboutToAppear(): void {
this.calcSelected();
}
aboutToAppear 是 ArkUI 组件的生命周期钩子,等价于 Flutter 的 initState()(在 Widget 初始化后、首次构建前调用)。这里调用 calcSelected() 确保初始状态下统计数据正确。
七、从 Flutter 迁移到 ArkUI 的常见陷阱
7.1 模板字符串与字符串拼接
Flutter/Dart 中广泛使用字符串插值:
Text('¥${item.price} × ${item.quantity}')
ArkTS 也支持模板字符串 ` `:
Text(`¥${item.price.toFixed(1)} × ${item.quantity}`)
但是,在 DevEco Studio Previewer 中,模板字符串有时会触发编译错误。更稳妥的做法是使用 + 拼接:
Text('¥' + item.price.toFixed(1) + ' × ' + item.quantity)
7.2 Row 的 .space() 属性
在 ArkUI API 10+ 中,Row 和 Column 支持 .space(value) 设置子元素间距。但在 API 9 或某些 Previewer 版本中,这个属性不存在。
解决办法是使用 Blank().width(n) 在子元素之间插入固定宽度的空白占位:
✅ Row() { Text('A'); Blank().width(8); Text('B'); }
❌ Row() { Text('A'); Text('B'); }.space(8)
7.3 @State 数组的深浅拷贝
这是最常被忽视的问题。@State 检测数组变化的方式是引用比较:
✅ this.shopItems = newArray; // 新引用 → 触发刷新
❌ this.shopItems[0] = newItem; // 修改已有引用 → 无变化
❌ this.shopItems.push(item); // 修改数组内容 → 无变化
7.4 ForEach 的 builder 参数
ArkTS 中 ForEach 的 builder 函数签名是 (item: T, index?: number) => void,而非 (context, index)。在函数体内直接调用 @Builder 或内联 ListItem。
正确写法:
ForEach(arr, (item, index?) => {
ListItem() { ... } // ← 直接渲染,不用 return
}, (item) => item.id.toString())
7.5 Button 的两种写法
ArkUI 中 Button 有两种构造方式:
// 方式一:标签构造(简洁)
Button('删除选中')
.fontSize(14)
.fontColor('#FFFFFF')
// 方式二:子组件构造(灵活)
Button() {
Text('删除选中')
.fontSize(14)
.fontColor('#FFFFFF')
}
方式一在 API 9+ 中支持 .fontSize() 和 .fontColor() 链式调用,但在更早的版本中可能不支持。如果遇到编译问题,改用方式二。
八、性能优化建议
8.1 ForEach 的 key
始终为 ForEach 提供第三个参数(key 生成函数),使用稳定的唯一标识符(如数据库 ID)而非数组索引:
ForEach(arr, ..., (item: ShopItem) => item.id.toString())
这样在列表增删时,ArkUI 可以精准定位哪些项需要更新,而不是重建整个列表。
8.2 避免不必要的重建
在 toggleItem 中,每次修改都重建整个数组。对于 8 项数据这不是问题,但如果列表有上千项,频繁重建会有性能开销。
优化方案:使用 @Observed 装饰的数据类 + @ObjectLink 子组件,实现"只修改属性、不重建数组"的更新模式。
8.3 @Observed + @ObjectLink 方案
对于大数据量列表,可以考虑 @Observed 方案:
@Observed
class ShopItem {
id: number = 0;
name: string = '';
price: number = 0;
quantity: number = 0;
selected: boolean = false;
constructor(id: number, name: string, price: number, quantity: number) {
this.id = id;
this.name = name;
this.price = price;
this.quantity = quantity;
}
}
然后在子组件中使用 @ObjectLink 建立深层观测:
@Component
struct ShopListItemView {
@ObjectLink item: ShopItem; // ← 深度观测 property 变化
build() {
ListItem() {
Checkbox()
.select(this.item.selected)
.onChange(() => { this.item.selected = !this.item.selected; })
// 直接修改属性 → 自动触发 UI 刷新
}
}
}
这种方案的优势是:不需要重建数组,直接修改 item.selected 即可触发 UI 刷新。缺点是代码结构更复杂,需要引入 @Observed 装饰的 class 和一个子 @Component。
8.4 列表性能监控
在 DevEco Studio 中,可以使用 Profile 工具监控列表的渲染性能。重点关注:
- 帧率(FPS):列表滚动时是否掉帧
- 布局耗时:
build()方法的执行时间 - 垃圾回收频率:频繁创建新对象是否导致频繁 GC
九、完整代码结构总览
├── entry/src/main/ets/pages/Index.ets ← 主页面(264行)
│ ├── interface ShopItem ← 数据模型
│ ├── @Component struct Index ← 页面组件
│ │ ├── @State shopItems ← 商品列表状态
│ │ ├── @State selectedCount ← 选中计数
│ │ ├── @State selectAll ← 全选状态
│ │ ├── aboutToAppear() ← 生命周期
│ │ ├── calcSelected() ← 更新统计
│ │ ├── toggleItem() ← 单选切换
│ │ ├── toggleSelectAll() ← 全选切换
│ │ ├── deleteSelected() ← 批量删除
│ │ ├── getSelectedTotal() ← 计算总价
│ │ └── build() ← UI 构建
│ │ ├── Row (标题 + 全选) ← Header
│ │ ├── List + ForEach + ListItem ← 列表区
│ │ └── Row (统计 + 按钮) ← 底部栏
│ └── ...(文件结束)
├── preview.html ← HTML 预览
└── README.md/GUIDE.md ← 本文档
十、总结与展望
10.1 核心收获
通过这个多选列表的实战案例,我们完成了从 Flutter 到 ArkUI 的完整迁移,核心收获包括:
-
组件映射:建立了
CheckboxListTile→ListItem(Row[Checkbox+Column+Text])的映射,理解了声明式 UI 的组合本质 -
状态管理:掌握了
@State+ 不可变数组模式的状态管理方案,理解了 ArkUI 响应式框架的刷新机制 -
Previewer 兼容:总结了 DevEco Studio Previewer 的编译限制,学会了内联渲染、字符串拼接等兼容技巧
-
布局系统:熟练运用
Row、Column、Flex.layoutWeight、Blank等布局组件,实现了与 Flutter 等效的弹性布局
10.2 拓展方向
本案例可以进一步拓展的功能:
- 搜索过滤:在列表顶部添加搜索框,动态过滤
shopItems - 长按拖拽排序:使用
List.onDrag实现拖拽排序 - 分页加载:结合列表滚动监听实现触底加载更多
- 多选工具栏:选中项出现浮动操作栏(FAB),集成更多批量操作
- 撤销功能:删除时保留备份,支持 “撤销删除”
- 数据持久化:使用
@StorageLink或@LocalStorageLink实现选中状态的持久化
10.3 对比总结
| 维度 | Flutter | ArkUI (HarmonyOS) | 难度 |
|---|---|---|---|
| 同等功能代码量 | ~200 行 | ~260 行 | 中等 |
| 学习曲线 | 需学习 Dart | 需学习 ArkTS | 相近 |
| 组件丰富度 | 非常丰富 | 持续增长中 | Flutter 胜 |
| Previewer 兼容 | 热重载极快 | Previewer 有限制 | Flutter 胜 |
| 系统集成能力 | 插件依赖 | 原生 API 直接调用 | ArkUI 胜 |
| 国内生态适配 | 一般 | 适配鸿蒙全场景 | ArkUI 胜 |
10.4 写在最后
ArkUI 作为鸿蒙生态的原生 UI 框架,虽然起步较晚,但其设计理念与 Flutter、SwiftUI、Jetpack Compose 等现代声明式 UI 框架一脉相承。对于有多端开发经验的工程师来说,ArkTS 的学习门槛并不高——核心在于理解声明式组件树的构建方式和响应式状态管理的刷新策略。
本案例虽然只是一个简单的多选列表,但它展示了 ArkUI 开发的核心范式:数据驱动 UI、声明式组合、响应式更新。掌握了这些基本模式,就可以以此为基础构建更复杂的应用场景。
希望这篇文章能帮助 Flutter 开发者快速上手 ArkUI,也希望能为正在学习鸿蒙开发的同行提供一份有参考价值的实战指南。
本文配套源码:entry/src/main/ets/pages/Index.ets
HTML 预览:preview.html
文章字数:约 10,200 字
更多推荐


所有评论(0)