HarmonyOS ArkUI 状态边界怎么拆:页面筛选、弹窗临时值和确认结果为什么不要混在一起

发布时间:2026/7/21 3:50:53
HarmonyOS ArkUI 状态边界怎么拆:页面筛选、弹窗临时值和确认结果为什么不要混在一起 HarmonyOS ArkUI 状态边界怎么拆页面筛选、弹窗临时值和确认结果为什么不要混在一起先说一个很常见的坑ArkUI 页面写到筛选、排序、弹窗这些地方时很容易把所有状态都塞进一个变量里。刚开始只有一个筛选条件看起来没问题后面多了分类、排序、价格区间、是否只看收藏页面就开始乱。最典型的现象是弹窗里点了几个条件还没点“确定”列表已经刷新了或者点了“取消”页面上的筛选条件却已经变了。这个问题不是弹窗组件难写而是“临时选择”和“确认结果”没有分开。这篇就专门拆这个边界。示例不绑死某个业务任何有筛选弹窗、排序面板、批量选择页面的 ArkUI 项目都能参考。环境和验证目标项说明页面模型Stage 模型 ArkTS 页面UI 方向ArkUI 声明式组件核心能力State、组件参数、事件回调、ForEach 稳定 key验证目标弹窗临时值不污染页面结果确认后才刷新列表适用页面搜索筛选页、收藏筛选页、订单筛选页、内容分类页这里不讨论复杂架构先把一个页面里最容易出错的状态边界讲清楚。错误写法弹窗直接改页面结果先看一个省事但很容易出问题的写法。Entry Component struct FilterBadPage { State category: string all; State sortType: string score; State showFilter: boolean false; build() { Column() { Button(打开筛选) .onClick(() { this.showFilter true; }) Text(当前分类 this.category) Text(当前排序 this.sortType) if (this.showFilter) { Column() { Button(家常菜) .onClick(() { this.category home; }) Button(收藏优先) .onClick(() { this.sortType favorite; }) Button(取消) .onClick(() { this.showFilter false; }) } } } } }这个写法的问题很明显弹窗里每点一次页面结果就被改一次。用户最后点取消页面状态也已经脏了。列表如果监听 category 和 sortType 重新查询还会提前刷新。所以筛选弹窗里至少要有两份状态- 页面已经确认的结果- 弹窗里正在试选的临时值。正确拆法页面结果和弹窗草稿分开我更推荐下面这种写法。页面只保存已确认结果弹窗打开时复制一份临时值。用户点确定临时值才写回页面。class FilterResult { category: string all; sortType: string score; constructor(category: string all, sortType: string score) { this.category category; this.sortType sortType; } copy(): FilterResult { return new FilterResult(this.category, this.sortType); } } Entry Component struct FilterBetterPage { State appliedFilter: FilterResult new FilterResult(); State draftFilter: FilterResult new FilterResult(); State showFilter: boolean false; private openFilterPanel() { this.draftFilter this.appliedFilter.copy(); this.showFilter true; } private applyFilter() { this.appliedFilter this.draftFilter.copy(); this.showFilter false; this.reloadList(); } private cancelFilter() { this.draftFilter this.appliedFilter.copy(); this.showFilter false; } private reloadList() { // 这里只根据 appliedFilter 查询列表 } build() { Column({ space: 12 }) { Button(打开筛选) .onClick(() this.openFilterPanel()) Text(当前分类 this.appliedFilter.category) Text(当前排序 this.appliedFilter.sortType) if (this.showFilter) { FilterPanel({ draft: this.draftFilter, onChange: (next: FilterResult) { this.draftFilter next; }, onConfirm: () this.applyFilter(), onCancel: () this.cancelFilter() }) } } .padding(16) } }这段代码的核心不是多写一个类而是把状态含义讲清楚状态含义什么时候改appliedFilter页面已经生效的筛选结果用户点确定后draftFilter弹窗里的临时选择用户在弹窗里切换选项时showFilter弹窗是否显示打开、取消、确定时弹窗组件只管临时值不直接查列表弹窗组件也不要知道列表怎么查询。它只负责展示当前草稿值并把新的草稿值回传给页面。Component struct FilterPanel { draft: FilterResult new FilterResult(); onChange: (next: FilterResult) void () {}; onConfirm: () void () {}; onCancel: () void () {}; private updateCategory(category: string) { const next this.draft.copy(); next.category category; this.onChange(next); } private updateSort(sortType: string) { const next this.draft.copy(); next.sortType sortType; this.onChange(next); } build() { Column({ space: 12 }) { Text(选择分类).fontSize(16).fontWeight(FontWeight.Medium) Row({ space: 8 }) { Button(全部).onClick(() this.updateCategory(all)) Button(家常菜).onClick(() this.updateCategory(home)) Button(硬菜).onClick(() this.updateCategory(main)) } Text(排序方式).fontSize(16).fontWeight(FontWeight.Medium) Row({ space: 8 }) { Button(推荐优先).onClick(() this.updateSort(score)) Button(收藏优先).onClick(() this.updateSort(favorite)) } Row({ space: 12 }) { Button(取消).onClick(() this.onCancel()) Button(确定).onClick(() this.onConfirm()) } } .padding(16) .backgroundColor(#FFFFFF) .borderRadius(12) } }这样写以后弹窗组件可以复用到别的页面。它不关心列表数据来源不关心 RDB 查询也不关心接口请求。它只处理一件事维护临时选择并把结果告诉外层。案例二排序和分页也要一起重置筛选状态确认后还有一个细节分页游标要不要重置。很多列表 bug 就出在这里。比如用户已经翻到第 4 页这时切换分类如果还拿旧分页游标继续查就可能出现重复数据、空列表、顺序乱跳。class PageQueryState { pageNo: number 1; pageSize: number 20; loading: boolean false; hasMore: boolean true; reset() { this.pageNo 1; this.loading false; this.hasMore true; } } Component struct FilterListPage { State appliedFilter: FilterResult new FilterResult(); State pageState: PageQueryState new PageQueryState(); State list: string[] []; private applyFilter(next: FilterResult) { this.appliedFilter next.copy(); this.pageState.reset(); this.list []; this.loadFirstPage(); } private loadFirstPage() { // 用 appliedFilter pageState.pageNo 查询第一页 } }这个动作看起来小但很重要。筛选条件变了列表的数据集合就变了分页状态也应该重新开始。否则页面表面上是新分类底层查询还带着旧游标。怎么验证这套拆法有没有问题我会按下面这几步测1. 打开筛选弹窗连续切换分类和排序不点确定页面列表不应该刷新。2. 点取消页面上显示的筛选结果不应该变化。3. 再打开弹窗切换条件后点确定页面结果才更新列表重新加载第一页。4. 已经滑到后几页时切换条件分页状态必须回到第一页。5. 同一个 FilterPanel 放到另一个页面只换外层 apply 逻辑不改弹窗组件。如果这五步都能过说明状态边界是清楚的。以后怎么避免这类问题我的习惯是先给状态命名不急着写 UI- 已经生效的结果用 applied、current、selected 这类名字- 弹窗里的临时选择用 draft、pending、temp 这类名字- 列表分页单独放 pageState不跟筛选条件混在一起- 子组件通过回调告诉父组件发生了什么不自己决定怎么查数据。这样写出来的代码读起来会更啰嗦一点但排查问题非常省时间。只要页面提前刷新就查 draft 有没有写回 applied只要分页乱就查筛选确认时有没有 reset pageState。问题不会散到一堆组件里。