鸿蒙原生应用实战(二):首页与包裹列表开发------List组件、ForEach渲染与状态管理
本文是系列第二篇,深入讲解快递追踪 App 首页的开发全过程,包括 List 列表渲染、@State 状态管理、条件渲染空状态、圆形状态标签以及页面路由导航等核心内容。
一、首页需求分析
首页是 App 的门面,我们的快递追踪 App 首页需要实现:
- 标题栏:显示"我的包裹"标题 + 右上角快捷操作图标(搜索、统计、公司管理)
- 包裹列表:展示所有包裹,每个卡片显示快递公司、单号、状态标签、备注、更新时间
- 状态区分:三种状态使用不同颜色标签------运输中(橙色)、已签收(绿色)、异常(红色)
- 空状态:无包裹时展示友好提示
- 点击跳转:点击卡片进入物流详情页
- 底部按钮:添加包裹按钮
二、数据结构设计
2.1 定义数据模型
在 ArkTS 中,我们使用 interface 定义数据结构:
typescript
// Index.ets --- 数据接口定义
interface PackageItem {
id: number;
trackingNo: string; // 快递单号
company: string; // 快递公司
status: string; // 状态码: transit | delivered | exception
statusText: string; // 状态文本: 运输中 | 已签收 | 异常
note: string; // 备注
updateTime: string; // 更新时间
events: TrackEvent[]; // 物流事件列表
}
interface TrackEvent {
time: string;
desc: string;
location: string;
}
2.2 为什么要把 status 和 statusText 分开?
status 是状态码(枚举值),用于逻辑判断和条件渲染;statusText 是展示文本。这样做的好处:
typescript
// 通过 status 状态码决定颜色和文本,而不是写死
backgroundColor(
item.status === 'transit' ? $r('app.color.status_transit') :
item.status === 'delivered' ? $r('app.color.status_delivered') :
$r('app.color.status_exception')
)
// 如果要国际化,只需替换状态文本,状态码不变
三、首页布局结构
3.1 整体框架
typescript
@Entry
@Component
struct Index {
@State packages: PackageItem[] = [ /* 模拟数据 */ ];
build() {
Column() {
// 1. 标题栏 Row
// 2. 包裹列表 List (或空状态)
// 3. 添加按钮 Button
}
.width('100%')
.height('100%')
.backgroundColor($r('app.color.background'))
}
}
三层布局:Column 作为垂直容器,从上到下排列标题栏、列表(可滚动)、底部按钮。
3.2 标题栏设计
typescript
Row() {
Text($r('app.string.title_home'))
.fontSize($r('app.float.page_title_font_size'))
.fontWeight(FontWeight.Bold)
.fontColor($r('app.color.text_primary'))
Blank() // 弹性空间,将右侧图标推到右边
Row() {
Text('🔍').onClick(() => { /* 跳转搜索页 */ })
Text('📊').onClick(() => { /* 跳转统计页 */ })
Text('🏢').onClick(() => { /* 跳转公司管理页 */ })
}
}
.width('100%')
.padding($r('app.float.padding_medium'))
.justifyContent(FlexAlign.SpaceBetween)
使用 Emoji 作为图标是一种快速原型的方式,生产环境建议替换为 SVG 图标组件。
3.3 空状态处理
typescript
if (this.packages.length === 0) {
Column() {
Text('📦').fontSize(60)
Text($r('app.string.no_packages'))
.fontSize($r('app.float.body_font_size'))
.fontColor($r('app.color.text_hint'))
.margin({ top: 16 })
}
.width('100%')
.height('80%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
} else {
// 列表渲染...
}
设计要点:
- 空状态居中显示,不干扰其他布局
- 使用大号 Emoji + 提示文案,视觉友好
height('80%')占满空间但给底部按钮留位置
四、List 列表组件详解
4.1 基础结构
typescript
List() {
ForEach(this.packages, (item: PackageItem) => {
ListItem() {
// 卡片内容
}
.onClick(() => { /* 跳转详情 */ })
}, (item: PackageItem) => item.id.toString())
}
.layoutWeight(1) // 列表占据剩余空间
4.2 ForEach 的第三个参数:keyGenerator
ForEach(arr, itemGenerator, keyGenerator) 的第三个参数非常关键:
typescript
ForEach(this.packages,
(item: PackageItem) => { /* 构建 UI */ },
(item: PackageItem) => item.id.toString() // ← key 生成器
)
- 提供稳定的 key,ArkTS 才能高效 diff 更新列表
- key 必须唯一且稳定,不推荐使用索引
- 没有 key 或 key 不稳定会导致列表闪烁或性能问题
4.3 卡片布局设计
每个包裹卡片是一个 Column 嵌套 Row:
typescript
ListItem() {
Column() {
// 第一行:公司名 + 状态标签
Row() {
Column() {
Text(item.company) // 公司名
Text(item.trackingNo) // 单号
}
.alignItems(HorizontalAlign.Start)
Blank()
// 状态标签 --- 圆角矩形背景
Text(item.statusText)
.fontSize($r('app.float.badge_font_size'))
.fontColor(Color.White)
.backgroundColor(
item.status === 'transit' ? $r('app.color.status_transit') :
item.status === 'delivered' ? $r('app.color.status_delivered') :
$r('app.color.status_exception')
)
.padding({ left: 10, right: 10, top: 4, bottom: 4 })
.borderRadius(12)
}
.width('100%')
// 条件渲染:备注
if (item.note.length > 0) {
Text(item.note)
}
// 更新时间
Text('更新: ' + item.updateTime)
}
.width('100%')
.padding($r('app.float.padding_medium'))
.backgroundColor($r('app.color.card_bg'))
.borderRadius($r('app.float.card_corner_radius'))
}
4.4 状态标签颜色映射
三种状态三种颜色,代码复用:
| 状态码 | 状态文本 | 颜色 | 色值 |
|---|---|---|---|
transit |
运输中 | 橙色 | #FFFF8C00 |
delivered |
已签收 | 绿色 | #FF4CAF50 |
exception |
异常 | 红色 | #FFF44336 |
通过三元运算符链实现条件颜色,代码简洁但要注意可读性。
五、@State 状态管理详解
5.1 @State 的作用
@State 是 ArkTS 中最核心的装饰器,标记的变量变化时会自动触发 UI 重渲染:
typescript
@Component
struct Index {
@State packages: PackageItem[] = [ /* ... */ ];
// 当 packages 的内容变化时,UI 自动更新
}
注意 :@State 监听的是引用变化,对于数组:
- ✅
this.packages = newArray→ 触发更新(赋值新数组) - ✅
this.packages.push(newItem)→ 触发更新(数组变异方法) - ❌
this.packages[0].note = 'xxx'→ 不触发更新(直接修改数组元素属性)
5.2 Array 操作的响应性
| 操作方式 | 是否触发 UI 更新 | 说明 |
|---|---|---|
this.packages = [] |
✅ 触发 | 赋值新数组 |
this.packages.push(x) |
✅ 触发 | 数组变异方法 |
this.packages.splice(i,1) |
✅ 触发 | 数组变异方法 |
this.packages[i] = x |
❌ 不触发 | 直接索引赋值 |
this.packages[i].note = x |
❌ 不触发 | 修改嵌套属性 |
解决方案:修改嵌套属性时使用对象展开创建新对象:
typescript
// 正确方式:创建新对象替换
let newItem = { ...this.packages[i], note: '新备注' };
let newArr = [...this.packages];
newArr[i] = newItem;
this.packages = newArr;
六、页面路由跳转
6.1 定义 RouteOpt 接口
typescript
interface RouteOpt {
url: string;
params?: Object;
}
6.2 无参数跳转
typescript
// 底部按钮 --- 跳转添加页
Button($r('app.string.btn_save'))
.onClick(() => {
let opt: RouteOpt = { url: 'pages/AddPackagePage' };
router.pushUrl(opt);
})
6.3 带参数跳转
typescript
// 点击卡片 --- 跳转详情页,携带包裹数据
ListItem() {
// ...
}
.onClick(() => {
let params = { packageData: item }; // 参数
let opt: RouteOpt = {
url: 'pages/TrackDetailPage',
params: params
};
router.pushUrl(opt);
})
6.4 接收参数(在详情页)
typescript
// TrackDetailPage.ets
aboutToAppear(): void {
const params = router.getParams() as Record<string, Object>;
if (params && params['packageData']) {
this.packageData = params['packageData'] as PackageItem;
}
}
重要安全措施:
- 使用
as Record<string, Object>断言 - 对
params判空 - 对
params['packageData']判空
这样即使在跳转时忘记传参,页面也不会崩溃,只会显示默认空数据。
七、实战技巧与避坑
7.1 响应式布局设计
使用 layoutWeight(1) 让列表占满剩余空间,避免底部按钮被挤出屏幕:
typescript
List() { /* ... */ }
.width('100%')
.layoutWeight(1) // ← 关键:占据 Column 中的剩余空间
// 底部按钮
Button('添加包裹')
.margin({ bottom: 16 }) // ← 底部安全间距
7.2 Card 设计规范
- 背景色 :白色
#FFFFFF(card_bg) - 圆角:12vp(card_corner_radius)
- 内边距:16vp(padding_medium)
- 列表项间距:通过 ListItem 的 padding 控制(上下 6vp)
- 阴影:鸿蒙的 Card 组件自带阴影效果
7.3 内联函数优化
图标点击的跳转逻辑可以内联写入,减少函数定义:
typescript
// 不推荐(额外函数)
searchClick() { router.pushUrl({ url: 'pages/SearchPage' }); }
Text('🔍').onClick(() => this.searchClick())
// 推荐(直接内联)
Text('🔍').onClick(() => {
let opt: RouteOpt = { url: 'pages/SearchPage' };
router.pushUrl(opt);
})
7.4 $r() 资源引用的优势
使用 $r('app.float.xxx') 而非硬编码尺寸:
typescript
// ✅ 使用资源引用,统一管理
.fontSize($r('app.float.body_font_size'))
.padding($r('app.float.padding_medium'))
// ❌ 硬编码,不利于维护
.fontSize(16)
.padding(16)
这样当需要调整全局字体或间距时,只需修改 float.json 一处即可。
八、完整首页代码结构
typescript
// pages/Index.ets --- 完整结构
import router from '@ohos.router';
interface RouteOpt { url: string; params?: Object; }
interface PackageItem { /* ... */ }
interface TrackEvent { /* ... */ }
@Entry
@Component
struct Index {
@State packages: PackageItem[] = [ /* 3条模拟数据 */ ];
build() {
Column() {
// 1. 标题栏 Row
// 2. 空状态 / 包裹列表 List
// 3. 添加按钮 Button
}
.width('100%').height('100%')
.backgroundColor($r('app.color.background'))
}
}

九、小结
本篇我们完成了首页的全部开发,核心要点:
- ✅ List + ForEach:列表渲染的标准范式,keyGenerator 的重要性
- ✅ @State 状态管理:响应式数据驱动 UI,数组操作的响应性限制
- ✅ 条件渲染:空状态 vs 列表的切换
- ✅ 路由跳转 :
router.pushUrl带参数跳转与接收 - ✅ 三种状态标签:通过状态码动态渲染颜色
- ✅ 资源引用 :
$r()统一管理字体、颜色、尺寸
下一篇将进入 表单交互与搜索筛选,涵盖添加包裹表单验证、搜索页面多条件筛选、快递公司管理等实战功能。
系列索引:
- 第一篇:项目初始化与工程架构
- 第二篇:首页与列表开发实战(本文)
- 第三篇:表单交互与搜索筛选
- 第四篇:物流时间线与历史记录
- 第五篇:数据统计与个人中心