ArkUI 列表性能实战|中式美食菜谱列表如何拆组件、控重绘和稳住滑动体验


中式美食里面有不少列表:菜谱列表、搜索结果、收藏页、购物清单、推荐卡片流。
列表这种页面看起来简单,其实很容易越写越重。刚开始数据少,直接在一个页面里把图片、标题、标签、按钮、收藏状态都写出来,问题不明显。等页面开始接搜索、收藏、最近浏览、购物清单这些状态以后,卡片会越来越复杂,刷新也会越来越频繁。
这篇只讲一个具体问题:ArkUI 列表怎么写得更稳一点。重点不是炫写法,而是把列表卡片拆清楚、把 key 设稳定、把状态范围收窄,让用户滑动时少抖动,开发调试时也能知道是哪一层在刷新。

本章位置

项目 内容
应用名称 中式美食
涉及页面 菜谱列表、搜索结果、收藏页、购物清单、推荐列表
主要问题 列表卡片复杂后,整页刷新、滑动抖动、状态互相影响
技术关键词 HarmonyOS、ArkUI、ArkTS、ForEach、@Builder、组件拆分、状态管理
本文目标 让列表更新更可控,卡片职责更清楚,性能问题更容易定位

本文导读

问题 本文做法
卡片代码写在页面里太长 拆成 RecipeCard、RecipeMetaRow、RecipeActionRow
列表刷新时容易整块重建 ForEach 使用稳定业务 id,不用 index 当核心 key
收藏、勾选、推荐理由都挤在一层 把页面状态、卡片状态、业务状态分开
滑动体验不好判断 用固定数据、快速筛选、连续滑动做验收
后续要接推荐排序 让列表卡片只消费展示模型,不直接算业务分数

列表页最怕什么

列表页最怕的不是代码多,而是"所有东西都混在一起"。
比如一个菜谱卡片里会有封面、标题、描述、标签、收藏按钮、最近浏览提示、购物清单入口。搜索结果页还要高亮关键词,推荐页还要显示推荐理由,购物清单页还要显示数量和勾选状态。如果这些全部塞在一个 build() 里,后面任何一个状态变化,都可能牵动整页。
我更倾向先把列表拆成三层:

层级 负责什么
Page 负责加载、筛选、空态、滚动容器
ViewModel 负责把业务数据转成列表展示模型
Card Component 只负责展示一条数据和触发用户动作

页面不要直接拼一堆业务判断。页面应该只问 ViewModel:现在要展示哪些卡片?卡片上要展示什么?用户点了收藏以后调用哪个动作?

不要让页面直接背所有字段

中式美食的菜谱数据本身字段不少:菜名、封面、分类、食材、步骤、营养、收藏状态、浏览状态、清单状态。列表页不需要知道所有字段。
更稳的做法是先做一个列表展示模型:

ts 复制代码
type RecipeListItem = {
  id: string
  title: string
  cover: ResourceStr
  desc: string
  tags: string[]
  favorite: boolean
  reason?: string
  actionText?: string
}

这层模型的好处很直接:页面不再关心菜谱完整结构,只关心列表需要的字段。详情页需要食材和步骤,列表页不一定需要。推荐排序需要分数,列表页也不应该直接拿分数做判断。
如果后续推荐页要显示"购物清单里已有部分食材",只需要给 reason 填一句话,卡片照样能展示。

RecipeCard 要拆出来

卡片一复杂,就不要继续留在页面里。中式美食这种内容型应用,菜谱卡片会在很多页面复用,拆组件能少很多重复。
一个基本拆法可以是这样:

ts 复制代码
@Component
struct RecipeCard {
  @Prop item: RecipeListItem
  onOpen: (id: string) => void = () => {}
  onFavorite: (id: string) => void = () => {}

  build() {
    Row() {
      Image(this.item.cover)
        .width(96)
        .height(96)
        .borderRadius(14)
        .objectFit(ImageFit.Cover)

      Column() {
        Text(this.item.title)
          .fontSize(18)
          .fontWeight(FontWeight.Medium)
          .maxLines(1)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text(this.item.desc)
          .fontSize(13)
          .fontColor('#6F6A5F')
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        RecipeTagRow({ tags: this.item.tags })

        if (this.item.reason) {
          Text(this.item.reason)
            .fontSize(12)
            .fontColor('#9A5A24')
        }
      }
      .layoutWeight(1)
    }
    .padding(12)
    .onClick(() => this.onOpen(this.item.id))
  }
}

这里有一个原则:卡片组件可以接收动作,但不要自己决定业务规则。比如"收藏以后写 Preferences 还是 RDB",这不是卡片的职责。卡片只负责把点击事件交出去。

ForEach 的 key 要稳定

列表刷新时,ForEach 的 key 很关键。很多人一开始会顺手用 index,这在静态列表里问题不大,但中式美食这种页面会筛选、排序、搜索、插入推荐位,用 index 就容易出问题。
比如搜索关键词变了,列表顺序变了。如果 key 只是 index,框架看到的可能是"第 1 个元素还是第 1 个元素",但实际内容已经换了。这样在复杂卡片里就容易出现状态错位、局部刷新不准、动画不自然。
更稳的写法是用业务 id:

ts 复制代码
ForEach(this.viewModel.items, (item: RecipeListItem) => {
  RecipeCard({
    item,
    onOpen: (id: string) => this.openDetail(id),
    onFavorite: (id: string) => this.toggleFavorite(id)
  })
}, (item: RecipeListItem) => item.id)

item.id 是菜谱自己的稳定标识。筛选、排序、推荐重排以后,只要还是同一道菜,key 就不变。这样框架更容易复用正确的节点,页面状态也更容易保持一致。

状态范围要收窄

列表页还有一个常见问题:一个很小的变化,引发整页跟着变。
比如用户点了某一道菜的收藏,如果页面只维护一个大数组,然后每次都重新生成所有卡片,短期能跑,长期会难调。更好的方式是把状态变化收窄到具体业务动作:

ts 复制代码
async toggleFavorite(recipeId: string) {
  await this.favoriteRepository.toggle(recipeId)

  this.items = this.items.map(item => {
    if (item.id !== recipeId) {
      return item
    }
    return {
      ...item,
      favorite: !item.favorite
    }
  })
}

这段代码看起来普通,但它传达了一个重点:收藏变化只影响命中的那一条展示模型。页面不用重新去算搜索关键词,不用重建全部推荐理由,也不用让卡片自己去读仓储。
如果后面数据量再变大,可以继续把 ViewModel 的更新逻辑拆得更细,但第一步先把"整页状态"和"单条卡片状态"分开。

标签行和操作行也可以继续拆

卡片内部也不要无限堆。标签行、按钮行、推荐理由都可以拆成更小的组件。

ts 复制代码
@Builder
function RecipeTagRow(tags: string[]) {
  Row() {
    ForEach(tags.slice(0, 3), (tag: string) => {
      Text(tag)
        .fontSize(11)
        .fontColor('#8A5A2B')
        .padding({ left: 8, right: 8, top: 3, bottom: 3 })
        .backgroundColor('#FFF3E4')
        .borderRadius(10)
    }, (tag: string) => tag)
  }
  .gap(6)
}

这里用 @Builder 就够了,不一定每个小片段都要做成完整 @Component。我的判断方式很简单:

写法 适合场景
@Builder 纯 UI 片段,没有自己的复杂状态
@Component 需要独立参数、事件、生命周期或复用频率高
普通函数 只做数据转换,不渲染 UI

中式美食里,RecipeCard 适合做组件,RecipeTagRow 可以先用 Builder。这样不会过度设计,也能保持卡片代码清爽。

图片加载要给稳定尺寸

列表里的图片一定要给稳定尺寸。菜谱卡片如果等图片加载完才撑开高度,滑动时就容易跳。
中式美食的封面图在列表里可以固定为 96x96 或 112x88,推荐流里可以统一为 16:9。关键不是哪个尺寸最好,而是同一个列表里的卡片尺寸要稳定。

ts 复制代码
Image(item.cover)
  .width(96)
  .height(96)
  .borderRadius(14)
  .objectFit(ImageFit.Cover)
  .backgroundColor('#F3EFE7')

背景色也要给。图片没加载出来时,卡片不会突然露白,也不会显得像坏图。

推荐列表也应该吃同一套卡片模型

第 29 篇讲了推荐排序。推荐排序算出来以后,不要让推荐页重新写一套卡片。
推荐页可以把排序结果转成同一个 RecipeListItem

ts 复制代码
function toListItem(item: RecipeRecommendation): RecipeListItem {
  return {
    id: item.recipe.id,
    title: item.recipe.name,
    cover: item.recipe.cover,
    desc: item.recipe.description,
    tags: item.recipe.tags,
    favorite: item.recipe.favorite,
    reason: item.reasons.slice(0, 2).join(' · '),
    actionText: '查看做法'
  }
}

这样做有两个收益:

收益 说明
UI 一致 推荐、搜索、收藏页的卡片不会长得各不相同
维护成本低 卡片样式改一次,多处一起生效
测试更清楚 只要测展示模型,不用每个页面重复测卡片细节
后续扩展容易 推荐理由只是模型字段,不会侵入卡片结构

工程验收记录

验收动作 预期结果
打开菜谱列表快速滑动 卡片高度稳定,没有图片加载后跳动
搜索关键词后刷新列表 列表顺序变化后,收藏状态不串行
连续点击收藏按钮 只更新当前卡片状态,不影响其它卡片
切换分类筛选 ForEach key 仍使用菜谱 id,卡片状态不乱
推荐列表展示理由 卡片能显示一到两条推荐理由,不挤压标题
删除或新增一条数据 列表能稳定增删,不依赖 index 识别业务项

这张表很重要。列表性能不能只靠"感觉还行"。至少要用这些动作去压一下,看看有没有抖动、串状态、错位、空白闪烁。

推荐阅读

主题 为什么建议一起看 链接
列表筛选体验 关键词命中和排序切换会直接触发列表刷新 阅读文章
购物清单合并 清单页也是列表,数量合并后更需要稳定 key 阅读文章
推荐排序 推荐结果最终还是要落到列表卡片里展示 阅读文章
本地持久化 收藏和最近浏览状态来自本地仓储 阅读文章

本章小结

中式美食的列表页要想稳,不能只看页面有没有渲染出来。真正要看的,是列表数据变化时,卡片是不是还稳定,收藏状态是不是还准确,图片尺寸是不是还撑得住,推荐理由会不会把卡片挤乱。
我会先做三件事:第一,把 RecipeCard 从页面里拆出来;第二,ForEach 使用菜谱 id 做稳定 key;第三,把页面加载状态、卡片展示模型和业务仓储动作分开。这样列表后面接搜索、收藏、购物清单、推荐排序时,不会每加一个能力就把页面改成一团。
最后留一个实际问题:如果你的 HarmonyOS 页面里也有复杂列表,你现在用的是 index 当 key,还是稳定业务 id?如果列表会筛选、排序或插入推荐位,我建议尽早改成业务 id,这个改动越早做,后面越省事。

相关推荐
Ww.xh1 小时前
DevEco Studio虚拟机部署指南
华为·harmonyos
yuanlaile1 小时前
前端转 Flutter 踩坑实录:一套可落地 Flutter 鸿蒙 App 完整学习路线,建议收藏!
flutter·harmonyos·flutter开发鸿蒙·跨平台开发app·flutter深入学习
爸爸6192 小时前
08 ArkTS 类与静态方法设计:单例、DAO 与设计令牌
华为·harmonyos·鸿蒙系统
ZZZMMM.zip2 小时前
基于鸿蒙HarmonyOS NEXT开发AI天气助手应用:智能出行新体验与鸿蒙Flutter框架跨端实践
人工智能·flutter·华为·harmonyos·鸿蒙系统
心中有国也有家2 小时前
AtomGit Flutter 鸿蒙客户端:一键记录情绪的极致体验
学习·flutter·华为·harmonyos
ZZZMMM.zip2 小时前
营养方程式-营养餐单规划的HarmonyOS开发实践
人工智能·华为·harmonyos·鸿蒙系统
不才难以繁此生3 小时前
HarmonyOS RDB 实战|中式美食菜谱表设计:食材、步骤与购物清单怎么落库
flutter·harmonyos·rdb·中式美食·本地持久化
小雨青年3 小时前
【HarmonyOS 7开发者前瞻】06 HarmonyOS 7 AI 能力接入前,先用业务场景判断优先级
人工智能·华为·harmonyos
TrisighT3 小时前
鸿蒙 ArkWeb 混合开发全攻略:JSBridge 双向通信 + 实战登录注入
前端·harmonyos·arkts