【ArkTS开发实战】从零实现行李清单应用,掌握Grid与List核心组件
【ArkTS开发实战】从零实现行李清单应用,掌握Grid与List核心组件
运行截图:



一、引言
在日常生活和旅行准备中,一份清晰、易用的行李清单能帮助我们避免遗漏重要物品。本文将带领读者使用 HarmonyOS 的 ArkTS 声明式 UI 开发框架,从零开始实现一款简洁美观的行李清单应用。通过这个项目,你将深入理解 ArkTS 中 Grid 网格布局组件和 List 列表组件的使用方法,掌握状态管理、数据驱动 UI 更新等核心概念。
本应用的最终效果如下:页面顶部使用 Grid 组件以四列网格的形式展示物品分类(衣物、证件、电子、洗护),点击某个分类后,下方的 List 列表会展示该分类下的所有物品,每个物品都带有一个可勾选的 Checkbox,勾选后物品名称会显示删除线效果,表示已装入行李。
二、项目环境准备
2.1 开发工具
在开始编码之前,请确保你已经安装了以下开发工具:
- DevEco Studio:HarmonyOS 官方集成开发环境,支持 ArkTS 语言的开发、调试和预览。建议安装最新版本的 DevEco Studio,以获得最佳的开发体验和最新的 API 支持。
- HarmonyOS SDK:DevEco Studio 通常会在安装过程中自动下载 SDK,请确保 SDK 版本与你的目标设备或模拟器兼容。
2.2 创建项目
打开 DevEco Studio,选择 Create Project,然后选择 Empty Ability 模板。填写项目名称(如 ArkTsPackinglist),包名等信息后,点击 Finish 即可创建一个基础的 HarmonyOS 项目。
项目创建完成后,主要的代码文件位于 entry/src/main/ets/pages/ 目录下,其中 Index.ets 是默认的首页文件,我们将在这个文件中实现行李清单的全部功能。
2.3 ArkTS 简介
ArkTS 是 HarmonyOS 的声明式 UI 开发语言,基于 TypeScript 扩展而来。它在 TypeScript 的基础上增加了声明式 UI 描述语法、状态管理装饰器(如 @State、@Prop、@Link 等)以及自定义组件的能力。使用 ArkTS,开发者可以通过简洁的声明式代码来描述 UI 的结构和样式,框架会自动根据状态的变化来更新 UI,无需手动操作 DOM。
ArkTS 的核心特性包括:
- 声明式 UI:通过链式调用描述组件的属性和样式,代码结构清晰、易于维护。
- 状态驱动:使用
@State等装饰器标记状态变量,当状态发生变化时,UI 会自动重新渲染。 - 丰富的组件库:提供了 Text、Image、Button、List、Grid、Column、Row 等丰富的内置组件,满足各种 UI 需求。
- TypeScript 兼容:完全兼容 TypeScript 的类型系统,支持接口、枚举、泛型等特性。
三、数据模型设计
在编写 UI 之前,我们首先需要设计好应用的数据模型。良好的数据模型设计是实现功能的基础,它决定了我们如何组织和操作数据。
3.1 定义分类枚举
行李清单中的物品按照用途可以分为不同的类别。我们使用 TypeScript 的 enum 关键字来定义分类枚举,这样可以确保分类值的类型安全,避免使用字符串常量时可能出现的拼写错误。
enum CategoryType {
Clothes,
Documents,
Electronics,
Toiletries
}
在上述枚举中,我们定义了四个分类:
| 枚举值 | 含义 | 对应物品示例 |
|---|---|---|
Clothes |
衣物 | T恤、裤子、外套、内衣、袜子 |
Documents |
证件 | 身份证、护照、机票、酒店预订单 |
Electronics |
电子 | 手机、充电器、耳机、充电宝、相机 |
Toiletries |
洗护 | 牙刷、牙膏、洗发水、沐浴露、毛巾 |
枚举的使用使得分类之间具有明确的区分,在后续的状态管理和条件判断中可以方便地进行比较。
3.2 定义物品数据接口
每个物品需要包含唯一标识、名称和勾选状态三个属性。我们使用 TypeScript 的 interface 来定义物品的数据结构:
interface PackingItem {
id: number;
name: string;
checked: boolean;
}
id:物品的唯一标识符,使用数字类型,用于在勾选操作中精确定位到某个物品。name:物品的名称,如"T恤"、"身份证"等,用于在界面上展示。checked:物品的勾选状态,true表示已勾选(已装入行李),false表示未勾选。
3.3 定义分类数据接口
每个分类包含分类类型、名称、图标和该分类下的物品列表。我们同样使用 interface 来定义:
interface Category {
type: CategoryType;
name: string;
icon: string;
items: PackingItem[];
}
type:分类的类型,使用前面定义的CategoryType枚举。name:分类的显示名称,如"衣物"、"证件"等。icon:分类的图标,这里使用 Emoji 表情符号来简化实现,实际项目中可以使用图片资源。items:该分类下的物品列表,是一个PackingItem数组。
通过这三个数据模型的定义,我们构建了一个清晰的层次结构:分类包含物品,每个分类和物品都有明确的属性。这种设计使得数据的存储、查询和修改都变得非常方便。
四、状态管理
4.1 @State 装饰器
在 ArkTS 中,@State 是最基础的状态管理装饰器。被 @State 标记的变量称为状态变量,当状态变量的值发生变化时,框架会自动检测这一变化,并重新渲染依赖该状态变量的 UI 组件。
在本应用中,我们定义了两个状态变量:
@State selectedCategory: CategoryType = CategoryType.Clothes;
@State categories: Category[] = [...];
selectedCategory:当前选中的分类类型,初始值为CategoryType.Clothes(衣物分类)。当用户点击不同的分类时,这个值会更新,从而触发下方物品列表的重新渲染。categories:所有分类及其物品的数据数组。当用户勾选某个物品时,我们会修改该数组中对应物品的checked属性,并重新赋值给categories,以触发 UI 更新。
4.2 状态更新的注意事项
在 ArkTS 中,对于数组和对象类型的状态变量,直接修改其内部属性(如 item.checked = true)可能不会触发 UI 更新。这是因为框架的状态变化检测机制是基于引用比较的。为了确保 UI 能够正确更新,我们需要在修改数据后重新赋值状态变量:
toggleItem(itemId: number) {
for (let category of this.categories) {
for (let item of category.items) {
if (item.id === itemId) {
item.checked = !item.checked;
// 重新赋值触发状态更新
this.categories = [...this.categories];
return;
}
}
}
}
在上述代码中,我们首先遍历所有分类和物品,找到目标物品并切换其 checked 属性。然后通过扩展运算符 [...this.categories] 创建一个新的数组并赋值给 this.categories,这样框架就能检测到状态的变化并触发 UI 更新。
这种"修改数据 + 重新赋值"的模式是 ArkTS 状态管理中的一个重要技巧,读者在实际开发中需要特别注意。
五、UI 布局实现
5.1 整体布局结构
应用的整体布局采用垂直方向的 Column 容器,从上到下依次包含三个部分:
- 标题区域:使用
Text组件显示"行李清单"标题。 - 分类网格区域:使用
Grid组件以四列网格展示四个物品分类。 - 物品列表区域:使用
List组件展示当前选中分类下的物品列表。
build() {
Column() {
// 标题
Text('行李清单')
...
// 分类Grid
Grid() { ... }
// 物品列表
List() { ... }
}
.width('100%')
.height('100%')
.backgroundColor('#FAFAFA')
}
Column 是 ArkTS 中最常用的布局容器之一,它将子组件按照垂直方向从上到下依次排列。通过设置 width('100%') 和 height('100%'),使整个布局占满屏幕。背景色设置为 #FAFAFA,这是一种浅灰色,比纯白色更加柔和,能提升视觉舒适度。
5.2 标题区域
标题区域使用 Text 组件实现,通过链式调用设置各项属性:
Text('行李清单')
.fontSize(24) // 字体大小 24fp
.fontWeight(FontWeight.Bold) // 粗体
.width('100%') // 宽度占满父容器
.textAlign(TextAlign.Center) // 文字居中
.padding(16) // 内边距 16vp
fontSize(24):设置字体大小为 24fp(font point),在 ArkTS 中字体大小的默认单位是 fp。fontWeight(FontWeight.Bold):设置字体为粗体,使标题更加醒目。textAlign(TextAlign.Center):设置文字水平居中对齐。padding(16):设置四周内边距为 16vp(virtual pixel),使标题与周围元素保持适当的间距。
5.3 分类网格区域(Grid)
Grid 组件是 ArkTS 中用于实现网格布局的容器组件,它可以将子组件按照行列的方式排列。在本应用中,我们使用 Grid 来展示四个物品分类。
5.3.1 Grid 组件的基本用法
Grid() {
ForEach(this.categories, (category: Category) => {
GridItem() {
Column() {
Text(category.icon)
.fontSize(32)
Text(category.name)
.fontSize(14)
.margin({ top: 8 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(this.selectedCategory === category.type ? '#E3F2FD' : '#F5F5F5')
.borderRadius(8)
.onClick(() => {
this.selectedCategory = category.type;
})
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsGap(12)
.columnsGap(12)
.height(120)
.padding({ left: 16, right: 16 })
5.3.2 columnsTemplate 列模板
columnsTemplate('1fr 1fr 1fr 1fr') 是 Grid 组件的核心属性之一,它定义了网格的列布局。1fr 表示一等份的空间,四个 1fr 表示将可用空间平均分成四列。这种写法类似于 CSS Grid 中的 grid-template-columns: 1fr 1fr 1fr 1fr。
如果你需要不同的列宽比例,可以修改 fr 的数值。例如,columnsTemplate('1fr 2fr 1fr') 会创建三列,中间列的宽度是两侧列的两倍。
5.3.3 GridItem 内容
每个 GridItem 内部包含一个 Column 容器,垂直排列图标和名称文字:
- 图标:使用
Text组件渲染 Emoji 表情,字体大小设为 32fp,使其在网格中足够醒目。 - 名称:使用
Text组件显示分类名称,字体大小 14fp,上边距 8vp,与图标保持适当间距。 - 居中对齐:通过
justifyContent(FlexAlign.Center)使 Column 内的子组件在交叉轴(水平方向)上居中对齐。
5.3.4 选中状态的高亮效果
为了给用户清晰的视觉反馈,我们实现了分类选中状态的高亮效果:
.backgroundColor(this.selectedCategory === category.type ? '#E3F2FD' : '#F5F5F5')
这里使用了三元表达式进行条件判断:
- 当分类被选中时(
this.selectedCategory === category.type),背景色设为#E3F2FD(浅蓝色)。 - 当分类未被选中时,背景色设为
#F5F5F5(浅灰色)。
这种颜色搭配既保持了界面的简洁,又能让用户一眼看出当前选中的是哪个分类。
5.3.5 点击事件
.onClick(() => {
this.selectedCategory = category.type;
})
为每个 GridItem 绑定了点击事件处理函数。当用户点击某个分类时,将 selectedCategory 状态变量更新为该分类的类型。由于 selectedCategory 是 @State 装饰的状态变量,它的变化会自动触发依赖它的 UI 组件重新渲染,从而实现物品列表的切换。
5.4 物品列表区域(List)
List 组件是 ArkTS 中用于展示垂直列表的容器组件,支持高效的列表复用机制,适合展示大量数据。
5.4.1 获取当前分类的物品
在渲染列表之前,我们需要一个方法来获取当前选中分类下的物品列表:
getCurrentItems(): PackingItem[] {
const category = this.categories.find(c => c.type === this.selectedCategory);
return category ? category.items : [];
}
这个方法使用数组的 find 方法在 categories 数组中查找与 selectedCategory 匹配的分类,然后返回该分类的 items 数组。如果未找到匹配的分类(理论上不会发生),则返回空数组。
5.4.2 List 组件的实现
List() {
ForEach(this.getCurrentItems(), (item: PackingItem) => {
ListItem() {
Row() {
Checkbox()
.select(item.checked)
.selectedColor('#2196F3')
.width(24)
.height(24)
.onChange((value: boolean) => {
this.toggleItem(item.id);
})
Text(item.name)
.fontSize(16)
.margin({ left: 12 })
.decoration({
type: item.checked
? TextDecorationType.LineThrough
: TextDecorationType.None
})
.fontColor(item.checked ? '#999999' : '#333333')
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.margin({ bottom: 8 })
}
})
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, top: 16 })
5.4.3 ForEach 数据渲染
ForEach 是 ArkTS 中用于根据数据数组批量生成组件的容器。它接收两个参数:数据源数组和组件生成函数。
ForEach(this.getCurrentItems(), (item: PackingItem) => {
// 为每个 item 生成一个 ListItem
})
当 getCurrentItems() 返回的数组发生变化时(例如切换了分类),ForEach 会自动更新列表内容,移除旧的列表项并添加新的列表项。
5.4.4 Checkbox 勾选框
每个物品列表项的左侧是一个 Checkbox 组件:
Checkbox()
.select(item.checked)
.selectedColor('#2196F3')
.width(24)
.height(24)
.onChange((value: boolean) => {
this.toggleItem(item.id);
})
select(item.checked):设置 Checkbox 的选中状态,与物品的checked属性绑定。selectedColor('#2196F3'):设置选中时的颜色为蓝色(Material Design 的主色调)。width(24)/height(24):设置 Checkbox 的尺寸为 24x24 vp。onChange:当用户点击 Checkbox 改变选中状态时触发。在回调函数中调用toggleItem方法更新物品的勾选状态。
5.4.5 物品名称的样式变化
物品名称的样式会根据勾选状态发生变化,这是提升用户体验的重要细节:
Text(item.name)
.fontSize(16)
.margin({ left: 12 })
.decoration({
type: item.checked
? TextDecorationType.LineThrough
: TextDecorationType.None
})
.fontColor(item.checked ? '#999999' : '#333333')
- 删除线效果:当物品被勾选时,通过
decoration属性添加LineThrough(删除线)效果,直观地表示该物品已准备好。注意decoration方法接收的是一个DecorationStyleInterface对象,需要传入{ type: TextDecorationType.LineThrough }而非直接传入枚举值。 - 颜色变化:勾选后文字颜色从深灰色(
#333333)变为浅灰色(#999999),配合删除线效果,进一步强化"已完成"的视觉感受。
5.4.6 layoutWeight 布局权重
.layoutWeight(1)
layoutWeight 是 List 组件的一个关键属性,它使得 List 在 Column 布局中占据剩余的可用空间。由于标题和 Grid 区域的高度是固定的,List 通过 layoutWeight(1) 自动填充页面剩余的所有垂直空间,确保列表可以完整展示所有物品。
六、交互逻辑详解
6.1 分类切换流程
分类切换是应用中最核心的交互之一,其完整流程如下:
- 用户操作:用户点击某个分类的 GridItem(例如点击"电子"分类)。
- 事件触发:GridItem 的
onClick事件处理函数被调用。 - 状态更新:
this.selectedCategory被赋值为CategoryType.Electronics。 - UI 更新 - Grid:由于
selectedCategory是状态变量,Grid 中每个 GridItem 的背景色条件表达式会重新计算,"电子"分类变为蓝色高亮,其他分类恢复灰色。 - UI 更新 - List:List 中的
ForEach调用getCurrentItems(),该方法根据新的selectedCategory返回电子分类的物品列表,List 自动更新显示手机、充电器、耳机、充电宝、相机等物品。
6.2 物品勾选流程
物品勾选的交互流程如下:
- 用户操作:用户点击某个物品旁边的 Checkbox。
- 事件触发:Checkbox 的
onChange回调被调用,参数value为新的选中状态。 - 调用 toggleItem:
onChange回调中调用this.toggleItem(item.id),传入物品的唯一 ID。 - 查找并修改数据:
toggleItem方法遍历所有分类和物品,找到 ID 匹配的物品,将其checked属性取反。 - 触发状态更新:通过
this.categories = [...this.categories]重新赋值状态变量。 - UI 更新:
- Checkbox 的选中状态更新(勾选或取消勾选)。
- 物品名称的文字装饰更新(添加或移除删除线)。
- 物品名称的颜色更新(灰色或深色)。
6.3 数据流总结
整个应用的数据流可以概括为:
用户操作 → 事件处理 → 状态变量更新 → UI 自动重新渲染
这种单向数据流的设计模式使得应用的状态变化可预测、可追踪,是声明式 UI 框架的核心思想。
七、关键知识点总结
7.1 Grid 组件核心属性
| 属性 | 说明 | 示例 |
|---|---|---|
columnsTemplate |
定义列宽比例 | '1fr 1fr 1fr 1fr' |
rowsTemplate |
定义行高比例 | '1fr 1fr' |
columnsGap |
列间距 | 12 |
rowsGap |
行间距 | 12 |
GridItem |
Grid 的子组件容器 | 必须作为 Grid 的直接子组件 |
7.2 List 组件核心属性
| 属性 | 说明 | 示例 |
|---|---|---|
layoutWeight |
在父布局中的权重 | 1 |
ListItem |
List 的子项容器 | 必须作为 List 的直接子组件 |
ForEach |
数据驱动的子组件生成器 | ForEach(dataArray, itemBuilder) |
7.3 状态管理要点
| 装饰器 | 用途 | 特点 |
|---|---|---|
@State |
组件自身的状态管理 | 状态变化触发 UI 更新 |
@Prop |
父到子的单向数据传递 | 子组件无法修改父组件的状态 |
@Link |
父子组件的双向数据同步 | 子组件的修改会同步到父组件 |
7.4 常用布局容器
| 容器 | 排列方向 | 适用场景 |
|---|---|---|
Column |
垂直(从上到下) | 页面整体布局 |
Row |
水平(从左到右) | 列表项内部布局 |
Grid |
网格(行列) | 分类展示、图片墙 |
List |
垂直列表 | 可滚动的数据列表 |
Stack |
层叠(后覆盖前) | 叠加效果、悬浮按钮 |
八、扩展建议
在完成基础版本后,你可以尝试以下扩展功能来进一步提升应用的功能性和用户体验:
8.1 添加物品管理功能
- 新增物品:在每个分类下添加输入框和按钮,允许用户自定义添加新的物品。
- 删除物品:通过左滑列表项显示删除按钮,实现物品的删除功能。
- 编辑物品:点击物品名称进入编辑模式,修改物品名称。
8.2 添加统计功能
- 在页面底部或标题栏显示当前分类的勾选进度,例如"已准备 3/5 项"。
- 显示所有分类的总体完成进度,帮助用户了解整体准备情况。
8.3 数据持久化
- 使用
@StorageProp和@StorageLink将勾选状态保存到AppStorage中。 - 使用文件存储或
Preferences(首选项)将数据持久化到本地,确保应用重启后数据不丢失。
8.4 新增分类功能
- 允许用户自定义新的物品分类,而不仅限于预设的四个分类。
- 支持为分类选择自定义图标和颜色。
8.5 多清单支持
- 支持创建多个行李清单(如"出差清单"、“旅行清单”),每个清单独立管理。
- 提供清单模板功能,预设常见场景的物品列表。
九、完整代码
以下是本应用的完整代码,供读者参考:
// 物品分类枚举
enum CategoryType {
Clothes,
Documents,
Electronics,
Toiletries
}
// 物品数据模型
interface PackingItem {
id: number;
name: string;
checked: boolean;
}
// 分类数据模型
interface Category {
type: CategoryType;
name: string;
icon: string;
items: PackingItem[];
}
@Entry
@Component
struct Index {
@State selectedCategory: CategoryType = CategoryType.Clothes;
@State categories: Category[] = [
{
type: CategoryType.Clothes,
name: '衣物',
icon: '👕',
items: [
{ id: 1, name: 'T恤', checked: false },
{ id: 2, name: '裤子', checked: false },
{ id: 3, name: '外套', checked: false },
{ id: 4, name: '内衣', checked: false },
{ id: 5, name: '袜子', checked: false }
]
},
{
type: CategoryType.Documents,
name: '证件',
icon: '📄',
items: [
{ id: 6, name: '身份证', checked: false },
{ id: 7, name: '护照', checked: false },
{ id: 8, name: '机票', checked: false },
{ id: 9, name: '酒店预订单', checked: false }
]
},
{
type: CategoryType.Electronics,
name: '电子',
icon: '📱',
items: [
{ id: 10, name: '手机', checked: false },
{ id: 11, name: '充电器', checked: false },
{ id: 12, name: '耳机', checked: false },
{ id: 13, name: '充电宝', checked: false },
{ id: 14, name: '相机', checked: false }
]
},
{
type: CategoryType.Toiletries,
name: '洗护',
icon: '🧴',
items: [
{ id: 15, name: '牙刷', checked: false },
{ id: 16, name: '牙膏', checked: false },
{ id: 17, name: '洗发水', checked: false },
{ id: 18, name: '沐浴露', checked: false },
{ id: 19, name: '毛巾', checked: false }
]
}
];
// 获取当前分类的物品列表
getCurrentItems(): PackingItem[] {
const category = this.categories.find(c => c.type === this.selectedCategory);
return category ? category.items : [];
}
// 切换物品勾选状态
toggleItem(itemId: number) {
for (let category of this.categories) {
for (let item of category.items) {
if (item.id === itemId) {
item.checked = !item.checked;
// 触发状态更新
this.categories = [...this.categories];
return;
}
}
}
}
build() {
Column() {
// 标题
Text('行李清单')
.fontSize(24)
.fontWeight(FontWeight.Bold)
.width('100%')
.textAlign(TextAlign.Center)
.padding(16)
// 分类Grid
Grid() {
ForEach(this.categories, (category: Category) => {
GridItem() {
Column() {
Text(category.icon)
.fontSize(32)
Text(category.name)
.fontSize(14)
.margin({ top: 8 })
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor(
this.selectedCategory === category.type
? '#E3F2FD' : '#F5F5F5'
)
.borderRadius(8)
.onClick(() => {
this.selectedCategory = category.type;
})
}
})
}
.columnsTemplate('1fr 1fr 1fr 1fr')
.rowsGap(12)
.columnsGap(12)
.height(120)
.padding({ left: 16, right: 16 })
// 物品列表
List() {
ForEach(this.getCurrentItems(), (item: PackingItem) => {
ListItem() {
Row() {
Checkbox()
.select(item.checked)
.selectedColor('#2196F3')
.width(24)
.height(24)
.onChange((value: boolean) => {
this.toggleItem(item.id);
})
Text(item.name)
.fontSize(16)
.margin({ left: 12 })
.decoration({
type: item.checked
? TextDecorationType.LineThrough
: TextDecorationType.None
})
.fontColor(item.checked ? '#999999' : '#333333')
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(8)
.margin({ bottom: 8 })
}
})
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16, top: 16 })
}
.width('100%')
.height('100%')
.backgroundColor('#FAFAFA')
}
}
十、总结
通过本文的学习,我们使用 ArkTS 实现了一款功能完整的行李清单应用。在这个过程中,我们深入掌握了以下核心知识点:
- 数据模型设计:使用
enum和interface构建了清晰的层次化数据结构。 - 状态管理:通过
@State装饰器实现了数据驱动的 UI 更新机制,理解了状态变量重新赋值触发 UI 刷新的重要性。 - Grid 网格布局:使用
Grid+GridItem实现了四列分类展示,掌握了columnsTemplate、rowsGap、columnsGap等核心属性。 - List 列表组件:使用
List+ListItem+ForEach实现了可滚动的物品列表,掌握了layoutWeight布局权重属性。 - Checkbox 交互:实现了物品的勾选功能,包括删除线效果和颜色变化等视觉反馈。
- 条件样式:通过三元表达式实现了选中状态的高亮效果和勾选状态的样式变化。
这些知识点是 ArkTS 开发中最基础也是最常用的内容,掌握了它们,你就具备了开发更多 HarmonyOS 应用的能力。希望本文能对你学习 ArkTS 和 HarmonyOS 开发有所帮助,祝你开发愉快!
更多推荐


所有评论(0)