
一、应用概述
在日常开发中,列表管理类应用是最基础也是最实用的应用类型之一。购物清单小应用作为一款轻量级的购物物品管理工具,能够帮助用户快速记录需要购买的商品,标记已购买状态,并实时统计购物进度。这个应用虽然功能简单,但涵盖了HarmonyOS ArkUI开发中的多个核心技术点,包括状态管理、列表渲染、用户交互、数据绑定等。
从技术架构角度来看,购物清单应用采用了典型的MVVM模式。View层负责UI展示,ViewModel层通过@State装饰器实现响应式数据绑定,Model层则通过自定义类来封装数据结构。这种架构设计使得数据变化能够自动驱动UI更新,大大简化了开发流程。
本应用的核心功能包括:商品添加、商品删除、购买状态切换、已选数量统计。通过这些功能的实现,我们可以深入学习Toggle组件的使用、List组件的渲染机制、以及数组操作在ArkTS中的最佳实践。
二、技术架构分析
2.1 整体架构设计
购物清单应用采用了分层架构设计,主要分为以下几层:
┌─────────────────────────────────────────────────────────┐
│ UI展示层 (View) │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 标题栏 │ │ 输入区域 │ │ 统计区域 │ │ 列表区域 │ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
├─────────────────────────────────────────────────────────┤
│ 状态管理层 (ViewModel) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ @State inputText_1: string │ │
│ │ @State items_1: ShoppingItem_1[] │ │
│ └──────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────┤
│ 数据模型层 (Model) │
│ ┌──────────────────────────────────────────────────┐ │
│ │ class ShoppingItem_1 │ │
│ │ – name_1: string │ │
│ │ – checked_1: boolean │ │
│ └──────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
2.2 数据流向分析
在ArkUI框架中,数据流是单向的,从状态变量流向UI组件。当用户进行操作时,触发事件处理函数修改状态变量,框架自动检测变化并重新渲染相关UI组件。
#mermaid-svg-GM26M2X5DousnVUn{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GM26M2X5DousnVUn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GM26M2X5DousnVUn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GM26M2X5DousnVUn .error-icon{fill:#552222;}#mermaid-svg-GM26M2X5DousnVUn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GM26M2X5DousnVUn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GM26M2X5DousnVUn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GM26M2X5DousnVUn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GM26M2X5DousnVUn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GM26M2X5DousnVUn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GM26M2X5DousnVUn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GM26M2X5DousnVUn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GM26M2X5DousnVUn .marker.cross{stroke:#333333;}#mermaid-svg-GM26M2X5DousnVUn svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GM26M2X5DousnVUn p{margin:0;}#mermaid-svg-GM26M2X5DousnVUn .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-GM26M2X5DousnVUn .cluster-label text{fill:#333;}#mermaid-svg-GM26M2X5DousnVUn .cluster-label span{color:#333;}#mermaid-svg-GM26M2X5DousnVUn .cluster-label span p{background-color:transparent;}#mermaid-svg-GM26M2X5DousnVUn .label text,#mermaid-svg-GM26M2X5DousnVUn span{fill:#333;color:#333;}#mermaid-svg-GM26M2X5DousnVUn .node rect,#mermaid-svg-GM26M2X5DousnVUn .node circle,#mermaid-svg-GM26M2X5DousnVUn .node ellipse,#mermaid-svg-GM26M2X5DousnVUn .node polygon,#mermaid-svg-GM26M2X5DousnVUn .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GM26M2X5DousnVUn .rough-node .label text,#mermaid-svg-GM26M2X5DousnVUn .node .label text,#mermaid-svg-GM26M2X5DousnVUn .image-shape .label,#mermaid-svg-GM26M2X5DousnVUn .icon-shape .label{text-anchor:middle;}#mermaid-svg-GM26M2X5DousnVUn .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GM26M2X5DousnVUn .rough-node .label,#mermaid-svg-GM26M2X5DousnVUn .node .label,#mermaid-svg-GM26M2X5DousnVUn .image-shape .label,#mermaid-svg-GM26M2X5DousnVUn .icon-shape .label{text-align:center;}#mermaid-svg-GM26M2X5DousnVUn .node.clickable{cursor:pointer;}#mermaid-svg-GM26M2X5DousnVUn .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GM26M2X5DousnVUn .arrowheadPath{fill:#333333;}#mermaid-svg-GM26M2X5DousnVUn .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GM26M2X5DousnVUn .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GM26M2X5DousnVUn .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GM26M2X5DousnVUn .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GM26M2X5DousnVUn .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GM26M2X5DousnVUn .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GM26M2X5DousnVUn .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GM26M2X5DousnVUn .cluster text{fill:#333;}#mermaid-svg-GM26M2X5DousnVUn .cluster span{color:#333;}#mermaid-svg-GM26M2X5DousnVUn div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GM26M2X5DousnVUn .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GM26M2X5DousnVUn rect.text{fill:none;stroke-width:0;}#mermaid-svg-GM26M2X5DousnVUn .icon-shape,#mermaid-svg-GM26M2X5DousnVUn .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GM26M2X5DousnVUn .icon-shape p,#mermaid-svg-GM26M2X5DousnVUn .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GM26M2X5DousnVUn .icon-shape .label rect,#mermaid-svg-GM26M2X5DousnVUn .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GM26M2X5DousnVUn .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GM26M2X5DousnVUn .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GM26M2X5DousnVUn :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}
用户输入商品名称
onChange事件触发
更新inputText_1状态
UI自动更新显示
用户点击添加按钮
onClick事件触发
创建ShoppingItem_1实例
使用concat添加到数组
items_1状态更新
List组件重新渲染
用户点击复选框
Toggle onChange触发
修改对应项的checked_1属性
状态更新触发UI刷新
文字样式变化
2.3 组件依赖关系
购物清单应用的组件结构清晰,各组件之间通过状态变量进行通信:
| Column | – | Row, Column | 根容器,垂直布局 |
| Row (标题栏) | Column | Button, Text | 显示标题和返回按钮 |
| TextInput | Column | – | 接收用户输入 |
| Button (添加) | Column | – | 触发添加商品操作 |
| Text (统计) | Column | – | 显示已选/总数统计 |
| List | Column | ListItem | 渲染商品列表 |
| ListItem | List | Row | 单个商品项容器 |
| Row (商品项) | ListItem | Toggle, Text, Button | 商品项内容布局 |
三、核心组件详解
3.1 Toggle组件深度解析
Toggle组件是HarmonyOS ArkUI中用于实现开关选择的组件,支持Checkbox、Button、Switch三种类型。在本应用中,我们使用Checkbox类型来实现商品的购买状态标记。
Toggle组件属性详解
Toggle({ type: ToggleType.Checkbox, isOn: item_1.checked_1 })
.onChange((checked_1: boolean) => {
this.items_1[index_1].checked_1 = checked_1;
})
上述代码中,Toggle组件的配置参数说明如下:
| type | ToggleType | 是 | 开关类型,可选Checkbox/Button/Switch |
| isOn | boolean | 是 | 初始状态,true表示选中 |
ToggleType枚举值说明:
| Checkbox | 复选框样式,支持多选 | 商品选择、任务完成标记 |
| Button | 按钮样式,按下时高亮 | 单选场景、模式切换 |
| Switch | 开关样式,滑动切换 | 设置开关、功能启用 |
Toggle组件常用属性
Toggle({ type: ToggleType.Checkbox, isOn: false })
.width(24) // 设置宽度
.height(24) // 设置高度
.selectedColor('#0A59F7') // 选中状态颜色
.switchPointColor('#FFFFFF') // Switch类型圆点颜色
.onChange((isOn: boolean) => {
// 状态变化回调
})
3.2 List组件渲染机制
List组件是HarmonyOS中用于展示长列表数据的核心组件,采用了虚拟滚动技术,只渲染可视区域内的列表项,大大提升了性能。
List组件基本结构
List() {
ForEach(this.items_1, (item_1: ShoppingItem_1, index_1: number) => {
ListItem() {
// 列表项内容
}
})
}
.width('90%')
.height(300)
.margin({ top: 8 })
ForEach渲染控制
ForEach是ArkUI提供的循环渲染指令,用于根据数组数据动态生成UI组件。其工作原理如下:
ForEach(
this.items_1, // 数据源
(item_1: ShoppingItem_1, index_1: number) => { // 项目生成函数
ListItem() {
// UI构建
}
}
)
ForEach的第三个参数是键值生成函数,用于优化列表更新性能:
ForEach(
this.items_1,
(item_1: ShoppingItem_1, index_1: number) => {
ListItem() { /* … */ }
},
(item_1: ShoppingItem_1, index_1: number) => item_1.name_1 + index_1.toString()
)
List组件性能优化
当列表数据量较大时,需要注意以下优化点:
List() {
// …
}
.width('90%')
.height(300)
.edgeEffect(EdgeEffect.Spring) // 边缘滑动效果
.chainAnimationOptions({ // 链式动画配置
minSpace: 10,
maxSpace: 30
})
.scrollBar(BarState.Auto) // 滚动条显示
3.3 TextInput组件详解
TextInput是文本输入组件,用于接收用户的键盘输入。在本应用中,用于输入商品名称。
TextInput({ placeholder: '添加购物商品…' })
.width('90%')
.height(48)
.margin({ top: 20 })
.onChange((value_1: string) => {
this.inputText_1 = value_1;
})
TextInput组件常用属性:
| placeholder | string | 占位提示文本 |
| type | InputType | 输入类型(Normal/Password/Email等) |
| maxLength | number | 最大输入长度 |
| enterKeyType | EnterKeyType | 回车键类型 |
| caretColor | ResourceColor | 光标颜色 |
| placeholderColor | ResourceColor | 占位符颜色 |
输入验证示例
TextInput({ placeholder: '请输入商品名称…' })
.type(InputType.Normal)
.maxLength(20)
.enterKeyType(EnterKeyType.Done)
.onChange((value: string) => {
// 实时输入验证
if (value.length > 0) {
this.inputText_1 = value.trim();
}
})
.onSubmit(() => {
// 回车提交
this.addItem();
})
四、状态管理详解
4.1 @State装饰器原理
@State是ArkUI中最基础的状态装饰器,被装饰的变量在发生变化时会自动触发UI刷新。其工作原理基于观察者模式:
@State inputText_1: string = '';
@State items_1: ShoppingItem_1[] = [
new ShoppingItem_1('牛奶', false),
new ShoppingItem_1('鸡蛋', true),
new ShoppingItem_1('面包', false),
new ShoppingItem_1('水果', false)
];
@State变量的限制
| 类型支持 | 支持string、number、boolean、Object、Array等类型 |
| 观察能力 | 仅能观察第一层数据变化 |
| 嵌套对象 | 嵌套对象属性变化不会触发刷新 |
| 数组操作 | push/pop不会触发刷新,需使用concat/filter |
4.2 数组操作最佳实践
在ArkTS中,直接修改数组元素或使用push/pop方法不会触发UI更新,需要返回新数组:
// ❌ 错误方式:直接修改不会触发更新
this.items_1.push(newItem);
// ✅ 正确方式:使用concat返回新数组
this.items_1 = this.items_1.concat([newItem]);
// ❌ 错误方式:splice不会触发更新
this.items_1.splice(index, 1);
// ✅ 正确方式:使用filter返回新数组
this.items_1 = this.items_1.filter((_, i_1: number) => i_1 !== index);
添加商品实现
Button('添加')
.width('90%')
.height(40)
.margin({ top: 12 })
.backgroundColor('#0A59F7')
.onClick(() => {
if (this.inputText_1 !== '') {
// 创建新的商品实例
let newItem_1 = new ShoppingItem_1(this.inputText_1, false);
// 使用concat添加到数组末尾
this.items_1 = this.items_1.concat([newItem_1]);
// 清空输入框
this.inputText_1 = '';
}
})
删除商品实现
Button('删除')
.width(60)
.height(32)
.fontSize(12)
.backgroundColor('#FF3B30')
.onClick(() => {
// 使用filter过滤掉指定索引的项
this.items_1 = this.items_1.filter((_, i_1: number) => i_1 !== index_1);
})
4.3 状态更新触发UI变化
当状态变量发生变化时,ArkUI框架会自动检测变化并重新渲染相关组件:
视图框架状态变量事件处理用户视图框架状态变量事件处理用户#mermaid-svg-BuswvAI38lJ6Dbzn{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BuswvAI38lJ6Dbzn .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BuswvAI38lJ6Dbzn .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BuswvAI38lJ6Dbzn .error-icon{fill:#552222;}#mermaid-svg-BuswvAI38lJ6Dbzn .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BuswvAI38lJ6Dbzn .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BuswvAI38lJ6Dbzn .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BuswvAI38lJ6Dbzn .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BuswvAI38lJ6Dbzn .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BuswvAI38lJ6Dbzn .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BuswvAI38lJ6Dbzn .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BuswvAI38lJ6Dbzn .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BuswvAI38lJ6Dbzn .marker.cross{stroke:#333333;}#mermaid-svg-BuswvAI38lJ6Dbzn svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BuswvAI38lJ6Dbzn p{margin:0;}#mermaid-svg-BuswvAI38lJ6Dbzn .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BuswvAI38lJ6Dbzn text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-BuswvAI38lJ6Dbzn .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-BuswvAI38lJ6Dbzn .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-BuswvAI38lJ6Dbzn .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-BuswvAI38lJ6Dbzn .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-BuswvAI38lJ6Dbzn #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-BuswvAI38lJ6Dbzn .sequenceNumber{fill:white;}#mermaid-svg-BuswvAI38lJ6Dbzn #sequencenumber{fill:#333;}#mermaid-svg-BuswvAI38lJ6Dbzn #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-BuswvAI38lJ6Dbzn .messageText{fill:#333;stroke:none;}#mermaid-svg-BuswvAI38lJ6Dbzn .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BuswvAI38lJ6Dbzn .labelText,#mermaid-svg-BuswvAI38lJ6Dbzn .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-BuswvAI38lJ6Dbzn .loopText,#mermaid-svg-BuswvAI38lJ6Dbzn .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-BuswvAI38lJ6Dbzn .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-BuswvAI38lJ6Dbzn .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-BuswvAI38lJ6Dbzn .noteText,#mermaid-svg-BuswvAI38lJ6Dbzn .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-BuswvAI38lJ6Dbzn .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BuswvAI38lJ6Dbzn .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BuswvAI38lJ6Dbzn .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-BuswvAI38lJ6Dbzn .actorPopupMenu{position:absolute;}#mermaid-svg-BuswvAI38lJ6Dbzn .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-BuswvAI38lJ6Dbzn .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-BuswvAI38lJ6Dbzn .actor-man circle,#mermaid-svg-BuswvAI38lJ6Dbzn line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-BuswvAI38lJ6Dbzn :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}点击添加按钮修改items_1数组状态变化通知重新渲染List组件显示新列表
五、UI布局实现
5.1 整体布局结构
购物清单应用采用Column容器作为根布局,内部嵌套Row和Column实现复杂布局:
Column (根容器)
│
├── Row (标题栏)
│ ├── Button (返回)
│ └── Text (标题)
│
└── Column (内容区域)
├── TextInput (输入框)
├── Button (添加按钮)
├── Text (统计信息)
└── List (商品列表)
└── ListItem × N
└── Row (商品项)
├── Toggle (复选框)
├── Text (商品名称)
└── Button (删除按钮)
5.2 标题栏实现
Row() {
Button('返回')
.onClick(() => router.back())
Text('购物清单小应用')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.textAlign(TextAlign.Center)
}
.width('100%')
.padding(12)
.backgroundColor('#F1F3F5')
标题栏使用Row容器实现水平布局,关键点说明:
| width | ‘100%’ | 占满父容器宽度 |
| padding | 12 | 内边距12vp |
| backgroundColor | ‘#F1F3F5’ | 浅灰色背景 |
Text组件使用layoutWeight(1)使其占据剩余空间,textAlign设置为Center使标题居中显示。
5.3 商品项布局
Row() {
Toggle({ type: ToggleType.Checkbox, isOn: item_1.checked_1 })
.onChange((checked_1: boolean) => {
this.items_1[index_1].checked_1 = checked_1;
})
Text(item_1.name_1)
.fontSize(14)
.layoutWeight(1)
.fontColor(item_1.checked_1 ? '#999999' : '#333333')
.opacity(item_1.checked_1 ? 0.6 : 1)
Button('删除')
.width(60)
.height(32)
.fontSize(12)
.backgroundColor('#FF3B30')
.onClick(() => {
this.items_1 = this.items_1.filter((_, i_1: number) => i_1 !== index_1);
})
}
.padding(12)
.width('100%')
商品项使用Row容器水平排列三个元素:复选框、商品名称、删除按钮。商品名称使用layoutWeight(1)占据中间的剩余空间。
5.4 条件样式绑定
商品名称根据购买状态显示不同样式:
Text(item_1.name_1)
.fontSize(14)
.layoutWeight(1)
.fontColor(item_1.checked_1 ? '#999999' : '#333333') // 已购买灰色
.opacity(item_1.checked_1 ? 0.6 : 1) // 已购买半透明
这种条件样式绑定是ArkUI响应式编程的典型应用,当checked_1状态变化时,文字颜色和透明度会自动更新。
六、样式定制技巧
6.1 颜色规范
应用采用统一的颜色规范:
| 主色调 | #0A59F7 | 蓝色,用于主要按钮 |
| 危险操作 | #FF3B30 | 红色,用于删除按钮 |
| 背景色 | #FFFFFF | 白色,主背景 |
| 次级背景 | #F1F3F5 | 浅灰色,标题栏背景 |
| 主文字 | #333333 | 深灰色,正常文字 |
| 次级文字 | #666666 | 中灰色,统计信息 |
| 禁用文字 | #999999 | 浅灰色,已购买商品 |
6.2 尺寸规范
| 标题文字 | 20fp | 标题栏文字 |
| 正文文字 | 14fp | 商品名称、统计信息 |
| 小号文字 | 12fp | 删除按钮文字 |
| 主按钮高度 | 40vp | 添加按钮 |
| 小按钮高度 | 32vp | 删除按钮 |
| 输入框高度 | 48vp | TextInput高度 |
| 列表项内边距 | 12vp | 商品项padding |
6.3 圆角与阴影
// 按钮圆角
Button('添加')
.borderRadius(8)
// 列表项圆角
Row() {
// …
}
.borderRadius(12)
.backgroundColor('#F8F9FA')
七、完整代码展示
import { router } from '@kit.ArkUI';
// 商品数据模型
class ShoppingItem_1 {
name_1: string = '';
checked_1: boolean = false;
constructor(name_1: string, checked_1: boolean) {
this.name_1 = name_1;
this.checked_1 = checked_1;
}
}
@Entry
@Component
struct ShoppingListApp {
// 输入框内容
@State inputText_1: string = '';
// 商品列表
@State items_1: ShoppingItem_1[] = [
new ShoppingItem_1('牛奶', false),
new ShoppingItem_1('鸡蛋', true),
new ShoppingItem_1('面包', false),
new ShoppingItem_1('水果', false)
];
build() {
Column() {
// 标题栏
Row() {
Button('返回')
.onClick(() => router.back())
Text('购物清单小应用')
.fontSize(20)
.fontWeight(FontWeight.Bold)
.layoutWeight(1)
.textAlign(TextAlign.Center)
}
.width('100%')
.padding(12)
.backgroundColor('#F1F3F5')
// 内容区域
Column() {
// 输入框
TextInput({ placeholder: '添加购物商品…' })
.width('90%')
.height(48)
.margin({ top: 20 })
.onChange((value_1: string) => {
this.inputText_1 = value_1;
})
// 添加按钮
Button('添加')
.width('90%')
.height(40)
.margin({ top: 12 })
.backgroundColor('#0A59F7')
.onClick(() => {
if (this.inputText_1 !== '') {
let newItem_1 = new ShoppingItem_1(this.inputText_1, false);
this.items_1 = this.items_1.concat([newItem_1]);
this.inputText_1 = '';
}
})
// 统计信息
Text('已选 ' + this.items_1.filter((i_1: ShoppingItem_1) => i_1.checked_1).length + '/' + this.items_1.length)
.fontSize(14)
.fontColor('#666666')
.margin({ top: 20, left: 20 })
.width('90%')
// 商品列表
List() {
ForEach(this.items_1, (item_1: ShoppingItem_1, index_1: number) => {
ListItem() {
Row() {
Toggle({ type: ToggleType.Checkbox, isOn: item_1.checked_1 })
.onChange((checked_1: boolean) => {
this.items_1[index_1].checked_1 = checked_1;
})
Text(item_1.name_1)
.fontSize(14)
.layoutWeight(1)
.fontColor(item_1.checked_1 ? '#999999' : '#333333')
.opacity(item_1.checked_1 ? 0.6 : 1)
Button('删除')
.width(60)
.height(32)
.fontSize(12)
.backgroundColor('#FF3B30')
.onClick(() => {
this.items_1 = this.items_1.filter((_, i_1: number) => i_1 !== index_1);
})
}
.padding(12)
.width('100%')
}
})
}
.width('90%')
.height(300)
.margin({ top: 8 })
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#FFFFFF')
}
}
八、组件对比分析
8.1 Toggle vs Checkbox
在HarmonyOS ArkUI中,Toggle组件的Checkbox类型与传统HTML中的checkbox元素功能相似,但提供了更丰富的定制能力:
| 样式定制 | 支持颜色、大小等属性 | 需要CSS覆盖 |
| 状态管理 | 内置isOn属性 | 需要checked属性 |
| 事件处理 | onChange回调 | onchange事件 |
| 动画效果 | 内置过渡动画 | 需要额外实现 |
8.2 List vs Scroll + Column
List组件相比Scroll + Column的组合,具有以下优势:
| 虚拟滚动 | ✅ 支持 | ❌ 不支持 |
| 性能 | 高(大数据量) | 低(全量渲染) |
| 滚动优化 | 内置优化 | 需手动实现 |
| 列表动画 | 支持 | 需手动实现 |
8.3 数据绑定方式对比
| @State | 组件内部状态 | 高 | 低 |
| @Prop | 父子组件传值 | 高 | 中 |
| @Link | 双向绑定 | 高 | 中 |
| @Observed | 嵌套对象观察 | 中 | 高 |
九、最佳实践总结
9.1 状态管理最佳实践
// ✅ 推荐:使用不可变数据模式
this.items_1 = this.items_1.concat([newItem]);
// ❌ 避免:直接修改数组
this.items_1.push(newItem);
// ✅ 推荐:使用filter删除
this.items_1 = this.items_1.filter((_, i) => i !== index);
// ❌ 避免:使用splice
this.items_1.splice(index, 1);
9.2 列表渲染优化
// ✅ 推荐:为ForEach提供key生成函数
ForEach(
this.items_1,
(item: ShoppingItem_1, index: number) => {
ListItem() { /* … */ }
},
(item: ShoppingItem_1) => item.name_1 // 使用唯一标识
)
9.3 输入验证
// ✅ 推荐:添加输入验证
.onClick(() => {
const trimmedText = this.inputText_1.trim();
if (trimmedText.length > 0 && trimmedText.length <= 50) {
let newItem = new ShoppingItem_1(trimmedText, false);
this.items_1 = this.items_1.concat([newItem]);
this.inputText_1 = '';
}
})
9.4 样式复用
// 使用@Styles装饰器复用样式
@Styles function commonButton() {
.width('90%')
.height(40)
.borderRadius(8)
}
Button('添加')
.commonButton()
.backgroundColor('#0A59F7')
十、扩展功能建议
10.1 数据持久化
使用Preferences API实现数据持久化存储:
import { preferences } from '@kit.ArkData';
// 保存数据
async saveItems(items: ShoppingItem_1[]) {
let prefs = await preferences.getPreferences(getContext(this), 'shopping');
await prefs.put('items', JSON.stringify(items));
await prefs.flush();
}
// 读取数据
async loadItems(): Promise<ShoppingItem_1[]> {
let prefs = await preferences.getPreferences(getContext(this), 'shopping');
let data = await prefs.get('items', '[]');
return JSON.parse(data as string);
}
10.2 分类管理
class ShoppingItem_1 {
name_1: string = '';
checked_1: boolean = false;
category_1: string = ''; // 新增分类字段
quantity_1: number = 1; // 新增数量字段
}
// 按分类分组显示
@State categories_1: string[] = ['食品', '日用品', '电子产品'];
10.3 批量操作
// 全选/取消全选
toggleAll() {
const allChecked = this.items_1.every(item => item.checked_1);
this.items_1 = this.items_1.map(item => {
item.checked_1 = !allChecked;
return item;
});
}
// 清除已完成
clearCompleted() {
this.items_1 = this.items_1.filter(item => !item.checked_1);
}
10.4 搜索过滤
@State searchText_1: string = '';
// 过滤显示
get filteredItems(): ShoppingItem_1[] {
if (!this.searchText_1) return this.items_1;
return this.items_1.filter(item =>
item.name_1.includes(this.searchText_1)
);
}
十一、性能优化策略
11.1 列表虚拟化
List组件默认启用虚拟滚动,但需要正确配置:
List() {
// …
}
.width('90%')
.height(300) // 必须设置固定高度
.cachedCount(5) // 缓存项数量
11.2 减少重渲染
// 使用@ObjectLink优化嵌套对象
@Observed
class ShoppingItem_1 {
name_1: string = '';
checked_1: boolean = false;
}
@Component
struct ItemComponent {
@ObjectLink item: ShoppingItem_1;
build() {
Row() {
// 只在item变化时重渲染此组件
}
}
}
11.3 懒加载
List() {
LazyForEach(this.dataSource, (item: ShoppingItem_1) => {
ListItem() {
// …
}
})
}
十二、总结
购物清单小应用虽然功能简单,但涵盖了HarmonyOS ArkUI开发的多个核心技术点。通过本篇博客的学习,我们掌握了以下知识点:
这些技术点在实际开发中非常常用,掌握它们对于构建更复杂的应用至关重要。在后续的博客中,我们将继续探索更多HarmonyOS开发技术,构建更加丰富的应用场景。
