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 菜谱、工具类或者内容列表页,可以先检查一件事:页面里的状态是不是只有一个可信来源。只要状态来源多了,低概率问题迟早会变成线上问题。

相关推荐
●VON1 小时前
HarmonyKit | 鸿蒙开发:hvigor 构建系统命令行与缓存机制详解
缓存·华为·交互·harmonyos·鸿蒙
星释1 小时前
鸿蒙智能体开发实战:35.鸿蒙壁纸大师 - 调用火山引擎模型生成壁纸
算法·华为·ai·harmonyos·鸿蒙·火山引擎
ZZZMMM.zip1 小时前
思维安检门-逻辑漏洞分析的HarmonyOS开发实践
人工智能·华为·harmonyos·鸿蒙·鸿蒙系统
爸爸6192 小时前
鸿蒙实战:手写板 WritePage 触摸绘图实现
华为·harmonyos·鸿蒙系统
在书中成长2 小时前
HarmonyOS 小游戏《对战五子棋》开发第35篇 - 列表渲染性能优化
harmonyos
xianjixiance_2 小时前
鸿蒙实战:SharePage 报告分享——雷达图预览与 pasteboard 剪贴板
华为·harmonyos
全堆鸿蒙13 小时前
27 RdbPredicates 条件查询详解:EqualTo、OrderBy、组合条件
华为·harmonyos·鸿蒙系统
心中有国也有家13 小时前
AtomGit Flutter 鸿蒙客户端: ChangeNotifier 模式
学习·flutter·华为·harmonyos
国服第二切图仔13 小时前
HarmonyOS APP《画伴梦工厂》开发第59篇-碰一碰精准分享——跨设备素材插入
华为·harmonyos