鸿蒙原生应用实战(二):首页与包裹列表开发——List组件、ForEach渲染与状态管理

鸿蒙原生应用实战(二):首页与包裹列表开发——List组件、ForEach渲染与状态管理 鸿蒙原生应用实战二首页与包裹列表开发——List组件、ForEach渲染与状态管理本文是系列第二篇深入讲解快递追踪 App 首页的开发全过程包括 List 列表渲染、State 状态管理、条件渲染空状态、圆形状态标签以及页面路由导航等核心内容。一、首页需求分析首页是 App 的门面我们的快递追踪 App 首页需要实现标题栏显示我的包裹标题 右上角快捷操作图标搜索、统计、公司管理包裹列表展示所有包裹每个卡片显示快递公司、单号、状态标签、备注、更新时间状态区分三种状态使用不同颜色标签——运输中橙色、已签收绿色、异常红色空状态无包裹时展示友好提示点击跳转点击卡片进入物流详情页底部按钮添加包裹按钮二、数据结构设计2.1 定义数据模型在 ArkTS 中我们使用interface定义数据结构// Index.ets — 数据接口定义interfacePackageItem{id:number;trackingNo:string;// 快递单号company:string;// 快递公司status:string;// 状态码: transit | delivered | exceptionstatusText:string;// 状态文本: 运输中 | 已签收 | 异常note:string;// 备注updateTime:string;// 更新时间events:TrackEvent[];// 物流事件列表}interfaceTrackEvent{time:string;desc:string;location:string;}2.2 为什么要把 status 和 statusText 分开status是状态码枚举值用于逻辑判断和条件渲染statusText是展示文本。这样做的好处// 通过 status 状态码决定颜色和文本而不是写死backgroundColor(item.statustransit?$r(app.color.status_transit):item.statusdelivered?$r(app.color.status_delivered):$r(app.color.status_exception))// 如果要国际化只需替换状态文本状态码不变三、首页布局结构3.1 整体框架EntryComponentstruct Index{Statepackages:PackageItem[][/* 模拟数据 */];build(){Column(){// 1. 标题栏 Row// 2. 包裹列表 List (或空状态)// 3. 添加按钮 Button}.width(100%).height(100%).backgroundColor($r(app.color.background))}}三层布局Column作为垂直容器从上到下排列标题栏、列表可滚动、底部按钮。3.2 标题栏设计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 空状态处理if(this.packages.length0){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 基础结构List(){ForEach(this.packages,(item:PackageItem){ListItem(){// 卡片内容}.onClick((){/* 跳转详情 */})},(item:PackageItem)item.id.toString())}.layoutWeight(1)// 列表占据剩余空间4.2 ForEach 的第三个参数keyGeneratorForEach(arr, itemGenerator, keyGenerator)的第三个参数非常关键ForEach(this.packages,(item:PackageItem){/* 构建 UI */},(item:PackageItem)item.id.toString()// ← key 生成器)提供稳定的 keyArkTS 才能高效 diff 更新列表key 必须唯一且稳定不推荐使用索引没有 key 或 key 不稳定会导致列表闪烁或性能问题4.3 卡片布局设计每个包裹卡片是一个Column嵌套RowListItem(){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.statustransit?$r(app.color.status_transit):item.statusdelivered?$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.length0){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运输中橙色#FFFF8C00delivered已签收绿色#FF4CAF50exception异常红色#FFF44336通过三元运算符链实现条件颜色代码简洁但要注意可读性。五、State 状态管理详解5.1 State 的作用State是 ArkTS 中最核心的装饰器标记的变量变化时会自动触发 UI 重渲染Componentstruct Index{Statepackages: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❌ 不触发修改嵌套属性解决方案修改嵌套属性时使用对象展开创建新对象// 正确方式创建新对象替换letnewItem{...this.packages[i],note:新备注};letnewArr[...this.packages];newArr[i]newItem;this.packagesnewArr;六、页面路由跳转6.1 定义 RouteOpt 接口interfaceRouteOpt{url:string;params?:Object;}6.2 无参数跳转// 底部按钮 — 跳转添加页Button($r(app.string.btn_save)).onClick((){letopt:RouteOpt{url:pages/AddPackagePage};router.pushUrl(opt);})6.3 带参数跳转// 点击卡片 — 跳转详情页携带包裹数据ListItem(){// ...}.onClick((){letparams{packageData:item};// 参数letopt:RouteOpt{url:pages/TrackDetailPage,params:params};router.pushUrl(opt);})6.4 接收参数在详情页// TrackDetailPage.etsaboutToAppear():void{constparamsrouter.getParams()asRecordstring,Object;if(paramsparams[packageData]){this.packageDataparams[packageData]asPackageItem;}}重要安全措施使用as Recordstring, Object断言对params判空对params[packageData]判空这样即使在跳转时忘记传参页面也不会崩溃只会显示默认空数据。七、实战技巧与避坑7.1 响应式布局设计使用layoutWeight(1)让列表占满剩余空间避免底部按钮被挤出屏幕List(){/* ... */}.width(100%).layoutWeight(1)// ← 关键占据 Column 中的剩余空间// 底部按钮Button(添加包裹).margin({bottom:16})// ← 底部安全间距7.2 Card 设计规范背景色白色#FFFFFFcard_bg圆角12vpcard_corner_radius内边距16vppadding_medium列表项间距通过 ListItem 的 padding 控制上下 6vp阴影鸿蒙的 Card 组件自带阴影效果7.3 内联函数优化图标点击的跳转逻辑可以内联写入减少函数定义// 不推荐额外函数searchClick(){router.pushUrl({url:pages/SearchPage});}Text().onClick(()this.searchClick())// 推荐直接内联Text().onClick((){letopt:RouteOpt{url:pages/SearchPage};router.pushUrl(opt);})7.4 $r() 资源引用的优势使用$r(app.float.xxx)而非硬编码尺寸// ✅ 使用资源引用统一管理.fontSize($r(app.float.body_font_size)).padding($r(app.float.padding_medium))// ❌ 硬编码不利于维护.fontSize(16).padding(16)这样当需要调整全局字体或间距时只需修改float.json一处即可。八、完整首页代码结构// pages/Index.ets — 完整结构importrouterfromohos.router;interfaceRouteOpt{url:string;params?:Object;}interfacePackageItem{/* ... */}interfaceTrackEvent{/* ... */}EntryComponentstruct Index{Statepackages: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()统一管理字体、颜色、尺寸下一篇将进入表单交互与搜索筛选涵盖添加包裹表单验证、搜索页面多条件筛选、快递公司管理等实战功能。系列索引第一篇项目初始化与工程架构第二篇首页与列表开发实战本文第三篇表单交互与搜索筛选第四篇物流时间线与历史记录第五篇数据统计与个人中心