ArkUI 筛选状态实战:中式美食列表页怎么让关键词、分类和排序不互相打架


用户先点"家常菜",再搜"豆腐",再切换"最近浏览",如果每个控件自己维护状态,就会出现列表标题和真实结果不一致。这不是页面上多写几个条件判断就能解决的事。中式美食这类应用,列表、详情、收藏、购物清单、推荐流会互相跳转,状态一乱,用户看到的就是"刚才点过的东西没了"。

应用名称 中式美食
本章主题 中式美食列表页怎么让关键词、分类和排序不互相打架
核心文件 entry/src/main/ets/features/recipe/RecipeListViewModel.ets
关键模块 RecipeListViewModel / RecipeFilterState
技术关键词 HarmonyOS,ArkUI,ArkTS,状态管理,中式美食

本章导读

这章只说一个真实工程问题:用户先点"家常菜",再搜"豆腐",再切换"最近浏览",如果每个控件自己维护状态,就会出现列表标题和真实结果不一致。

先看哪里 为什么要看
状态入口 先确认哪些动作会改数据
Repository 边界 决定查询和写入放在哪里
ViewModel 输出 页面拿到的是可直接渲染的数据
失败兜底 出错时不能让旧列表假装是新结果

当前验证环境

项目 版本或说明
DevEco Studio 6.x 系列
HarmonyOS SDK API 12/ArkTS 工程
UI 框架 ArkUI 声明式开发
数据层 Preferences + RDB relationalStore
验证设备 模拟器与真机页面流转

先把边界说清楚

我的处理方式是把状态先收口,再让页面渲染。页面不直接拼查询条件,组件也不偷偷改业务字段。这样写前面会慢一点,但后面加搜索、收藏、推荐、购物清单联动时,问题会少很多。

层级 应该负责 不要负责
Page 布局、点击、加载态展示 直接拼 SQL 或拼存储 key
ViewModel 组合页面状态,暴露可渲染模型 操作底层表结构
Repository 查询、写入、事务、去重 关心按钮颜色和页面文案
Model 稳定字段和默认值 临时 UI 动画状态
ts 复制代码
export interface RecipeFilterState {
  keyword?: string
  categoryId?: string
  recipeId?: string
  checkedOnly?: boolean
  sortBy?: 'default' | 'recent' | 'favorite'
}

数据模型先定下来

我不建议一开始就把所有字段塞到一个对象里。中式美食这里会先分两类:一类是业务字段,一类是页面展示字段。业务字段要稳定,展示字段可以后面再算。

ts 复制代码
export interface RecipeListItem {
  id: string
  recipeId: string
  title: string
  categoryName: string
  updatedAt: number
  checked: boolean
}
字段 用途 踩坑点
id 列表稳定 key 不能用 index 顶替
recipeId 回到菜谱来源 不然详情页不知道来源
categoryName 分类展示和筛选 不要只存展示文案
updatedAt 排序和最近记录 时间戳要统一单位
checked 勾选或状态标识 刷新后要能恢复

Repository 不要把脏数据交给页面

页面最怕拿到半成品。Repository 要把默认值、空数组、重复记录先处理好,再交给 ViewModel。这样页面写起来会笨一点,但后面问题少很多。

ts 复制代码
export class RecipeFilterStateRepository {
  async query(input: RecipeFilterState): Promise<RecipeListItem[]> {
    const rows = await this.queryRows(input)
    return rows.map(row => this.toModel(row))
  }

  private toModel(row: Record<string, Object>): RecipeListItem {
    return {
      id: String(row.id ?? ''),
      recipeId: String(row.recipeId ?? ''),
      title: String(row.title ?? ''),
      categoryName: String(row.categoryName ?? '未分类'),
      updatedAt: Number(row.updatedAt ?? 0),
      checked: Boolean(row.checked ?? false),
    }
  }
}

为什么不让页面自己兜底?因为页面兜底多了之后,每个入口都会有一套临时补丁。搜索页补一次,收藏页补一次,推荐页再补一次,同一道菜就会在不同页面显示成不同状态。

ViewModel 只输出页面能直接用的结果

ViewModel 的目标不是炫技,而是让页面少判断。中式美食页面多,列表、详情、收藏、购物清单都在读相似数据,输出模型越稳定,后面复用越舒服。

ts 复制代码
@Observed
export class RecipeFilterStateViewModel {
  items: RecipeListItem[] = []
  loading: boolean = false
  errorMessage: string = ''

  async reload(query: RecipeFilterState) {
    this.loading = true
    this.errorMessage = ''
    try {
      this.items = await this.repository.query(query)
    } catch (err) {
      this.errorMessage = '数据暂时没取到,可以稍后再试'
    } finally {
      this.loading = false
    }
  }
}
页面状态 用户看到什么 开发要检查什么
loading 骨架或加载文案 是否覆盖旧请求
empty 明确告诉用户没有结果 查询条件是否真的为空
error 允许重试 错误是否被吞掉
ready 稳定列表 key 是否稳定

ArkUI 页面不要偷改业务状态

ArkUI 写起来很方便,但也容易在组件里顺手改数据。我的习惯是:组件只发动作,ViewModel 决定怎么改。这样后面加埋点、加缓存、加失败回滚都还有位置。

ts 复制代码
@Component
struct RecipeListItemList {
  @ObjectLink vm: RecipeFilterStateViewModel

  build() {
    List() {
      ForEach(this.vm.items, (item: RecipeListItem) => {
        ListItem() {
          Text(item.title).fontSize(16).fontWeight(FontWeight.Medium)
        }
      }, (item: RecipeListItem) => item.id)
    }
  }
}

这里的 key 一定要用业务 id。不要用 index。index 在筛选、排序、插入、删除之后都会变,列表复用就容易把上一条状态带到下一条。

常见问题复盘

问题 表现 处理方式
旧请求覆盖新请求 搜索后又跳回旧列表 请求带版本号或时间戳
key 不稳定 勾选状态串行 ForEach 使用业务 id
空态不清楚 用户不知道是没数据还是加载失败 emptyReason 单独输出
查询散落在页面 每个入口逻辑不一样 收回 Repository
文案和真实条件不一致 标题显示分类 A,列表是全部 筛选状态只保留一份
ts 复制代码
let requestVersion = 0

async function safeReload(query: RecipeFilterState) {
  const current = ++requestVersion
  const next = await repository.query(query)
  if (current !== requestVersion) {
    return
  }
  this.items = next
}

工程验收

我会按下面这几项验收,不只看页面能不能跑起来。

验收项 操作 通过标准
首次进入 打开目标页面 loading 到 ready 不闪旧数据
条件切换 连续切关键词、分类、排序 最终列表和标题一致
空结果 输入不存在的菜名 空态文案清楚,不显示旧列表
返回页面 从详情页返回 原筛选条件还在
勾选或收藏 改一条状态后刷新 状态不串到别的条目
ts 复制代码
expect(vm.loading).toBe(false)
expect(vm.items.every(item => item.id.length > 0)).toBeTruthy()
expect(new Set(vm.items.map(item => item.id)).size).toBe(vm.items.length)

本章小结

中式美食列表页怎么让关键词、分类和排序不互相打架这类问题,不是多写几个 if 就能解决。中式美食现在的处理方式是把查询、状态和展示拆开:Repository 负责干净数据,ViewModel 负责页面模型,ArkUI 组件只负责展示和触发动作。
如果你也在做 HarmonyOS 菜谱、工具类或者内容列表页,可以先检查一件事:页面里的状态是不是只有一个可信来源。只要状态来源多了,低概率问题迟早会变成线上问题。

相关推荐
GitCode官方2 小时前
openPangu-2.0-Pro 模型及技术报告正式开源上线 AtomGit AI
人工智能·华为·开源·harmonyos·atomgit
想你依然心痛3 小时前
HarmonyOS 5.0智慧农业开发实战:构建分布式农业物联网与区块链农产品溯源系统
人工智能·分布式·物联网·区块链·智慧农业·harmonyos·开发实战
想你依然心痛17 小时前
ArkTS 布局系统概述——从线性到网格,掌握声明式 UI 的骨架艺术
harmonyos·arkts·flex弹性布局·声明式布局·grid网格布局·响应式适配·布局性能优化
程序员黑豆20 小时前
鸿蒙应用开发:@Computed 装饰器详解与实战
前端·harmonyos
ldsweet20 小时前
HarmonyOS NEXT 音频播放器开发:AVPlayer 封装、播放列表与后台播放实战
华为·音视频·harmonyos
qizayaoshuap21 小时前
# 44号应用:标签管理 — Flex 流式标签与交互状态设计
华为·harmonyos
哎呦喂我去去去1 天前
HarmonyOS SDK助力讯飞听见App能力建设
华为·harmonyos
lilian2331 天前
# Harmony os 技术实战|拼豆制图05:让 50 张本地图纸搜得准、排得稳
华为·harmonyos
想你依然心痛1 天前
Slider 滑块组件深度解析与交互定制全攻略
harmonyos·arkui·slider·双向绑定·sliderchange·sliderange·contentmodifier
想你依然心痛1 天前
React+百度地图高阶封装:hooks版轻量化地图解决方案
百度地图·状态管理·react hooks·组件化·地图sdk·高阶封装·工程化开发