HarmonyOS RDB 分页重复加载怎么办:中式美食菜谱列表怎么用游标、防抖和去重稳住下一页

发布时间:2026/7/17 4:32:06
HarmonyOS RDB 分页重复加载怎么办:中式美食菜谱列表怎么用游标、防抖和去重稳住下一页 先把问题说清楚中式美食的菜谱列表不是一次性全量展示。首页、分类页、搜索页都会从 RDB 里按条件取一批数据用户继续往下滑再取下一批。这个逻辑看起来很普通但真正写起来容易出两个问题一是同一道菜重复出现二是切换分类或关键词以后上一轮分页结果还会塞回当前列表。我一开始也想过用简单页码处理第一页查offset0第二页查offset20。但在真实页面里列表不是静止的。收藏状态会变分类条件会变搜索词会变返回页面时还要恢复现场。只盯着页码很容易把“当前查询条件下的下一页”和“上一轮查询残留的下一页”混成一件事。排查点真实表现连续触底一次滚动触发两次加载第二次拿到重复数据切换分类分类已经变了旧分类的下一页还在回来返回列表页面恢复了滚动位置但分页游标没有恢复收藏刷新单条状态更新后重新合并recipeId 重复我最后拆成三个状态分页不能只看pageIndex。在中式美食里我更愿意把它拆成三个状态当前查询条件、当前游标、正在执行的请求版本。typeRecipeQuery{keyword:stringcategoryId:stringsortMode:default|favorite|recent}typePageCursor{lastScore?:numberlastRecipeId?:stringpageSize:numberhasMore:boolean}typePageRequestState{queryKey:stringrequestVersion:numberloading:boolean}queryKey是关键。只要关键词、分类或排序变了就生成新的queryKey。旧请求即使回来也不能再改当前列表。RDB 查询边界不要只靠 offset如果数据排序稳定offset能用但推荐排序、收藏优先、最近浏览这些排序会让列表顺序发生变化。中式美食里我更倾向于用“上一条记录的排序值 recipeId”当游标这样下一页知道自己从哪里继续。asyncfunctionqueryNextRecipes(db:relationalStore.RdbStore,query:RecipeQuery,cursor:PageCursor){constpredicatesnewrelationalStore.RdbPredicates(recipe).equalTo(category_id,query.categoryId).limit(cursor.pageSize)if(query.keyword.trim()){predicates.like(name,%${query.keyword.trim()}%)}if(cursor.lastScore!undefinedcursor.lastRecipeId){predicates.beginWrap().lessThan(score,cursor.lastScore).or().beginWrap().equalTo(score,cursor.lastScore).greaterThan(recipe_id,cursor.lastRecipeId).endWrap().endWrap()}predicates.orderByDesc(score).orderByAsc(recipe_id)returndb.query(predicates)}这段代码的重点不是某个 API 多高级而是排序必须稳定。只按score排如果两道菜分数一样下一页边界就会晃加上recipe_id以后边界才稳定。合并列表前先按 recipeId 去重分页结果回来后不要直接list.push(...next)。中式美食里同一道菜可能从搜索、推荐、收藏状态刷新里回来所以合并前要按业务主键去重。functionmergeRecipePage(current:RecipeItem[],next:RecipeItem[]){constmapnewMapstring,RecipeItem()for(constitemofcurrent){map.set(item.recipeId,item)}for(constitemofnext){constoldmap.get(item.recipeId)map.set(item.recipeId,old?{...old,...item}:item)}returnArray.from(map.values())}这里不要用下标去重。下标只说明它在当前列表第几位不说明它是哪一道菜。列表一排序下标马上就失效。请求版本用来挡住旧结果下拉加载和切换分类经常会撞在一起。我的处理方式是每次条件变化都让requestVersion加一接口回来后先比版本不是当前版本就丢掉。classRecipeListViewModel{privaterequestVersion0privaterecipes:RecipeItem[][]privatecursor:PageCursor{pageSize:20,hasMore:true}asyncreload(query:RecipeQuery){constversionthis.requestVersionthis.cursor{pageSize:20,hasMore:true}constpageawaitthis.repository.queryFirstPage(query,this.cursor)if(version!this.requestVersion)returnthis.recipespage.itemsthis.cursorpage.cursor}asyncloadMore(query:RecipeQuery){if(!this.cursor.hasMore)returnconstversionthis.requestVersionconstpageawaitthis.repository.queryNextPage(query,this.cursor)if(version!this.requestVersion)returnthis.recipesmergeRecipePage(this.recipes,page.items)this.cursorpage.cursor}}这样做以后页面不会因为旧请求回来而闪一下也不会把旧分类的数据塞进新分类。ArkUI 触底也要做防抖ArkUI 列表触底时滚动状态可能连续触发。这里不要把所有保护都压给 Repository页面层也要挡住重复触发。List({scroller:this.scroller}){ForEach(this.recipes,(recipe:RecipeItem){RecipeCard({recipe,onOpen:this.openDetail})},(recipe:RecipeItem)recipe.recipeId)}.onReachEnd((){if(this.loadingMore||!this.hasMore)returnthis.loadingMoretruethis.viewModel.loadMore(this.query).finally((){this.loadingMorefalse})})这里的ForEach key也要用recipeId不能用 index。分页列表一合并用 index 做 key 会让旧卡片复用到新菜谱上最后表现出来就是图片、收藏状态或者点击事件串行。我会怎么验收操作预期结果连续快速滑到底部只进入一次加载中不重复追加同一页加载中切换分类旧请求回来后不改新分类列表收藏一条菜谱再继续加载收藏状态保留列表不重复从详情页返回滚动位置和已有分页结果不乱清空关键词游标重置列表从默认条件重新查最后我把这个问题归到一句话分页不是简单的“下一页”。它其实是查询条件、排序边界、请求时序和列表合并共同决定的结果。中式美食这种菜谱应用只要有分类、搜索、收藏和推荐排序就必须把这几块拆清楚不然后面页面越做越多重复加载会越来越难查。为什么不能只在 UI 层挡一下这个问题最容易被临时修成“按钮防连点”或者“触底时加一个 loading”。这两个保护要有但它们只能挡住一部分重复触发挡不住旧请求回来也挡不住排序边界漂移。中式美食的列表来源比较多首页推荐、分类筛选、搜索结果和收藏状态都会影响同一组菜谱只有 UI 层加锁最后还是会在数据合并时出问题。临时做法看起来解决了什么后面为什么还会出问题触底时加 loading连续触底少触发一次旧请求回来仍然可能覆盖新列表只用 pageIndex能取到下一页排序变化后 offset 边界不稳定合并时直接 push代码最短同一道菜从不同入口回来会重复切换分类时清数组页面看起来刷新了未完成请求仍然可能把旧数据塞回来所以我的处理顺序是先让查询条件可比较再让下一页边界稳定最后在合并时按recipeId做兜底。这样就算其中一层漏了一次也不至于直接把重复数据展示给用户。为什么用 recipeId 做最后兜底菜谱列表里的下标不是身份。今天这道菜排第 3明天因为收藏、评分、最近浏览变成第 8这都很正常。真正能代表同一道菜的只有业务主键。中式美食里我把recipeId当成列表合并、收藏回写、详情跳转和最近浏览记录的同一条线这样排查时不会每个模块都用一套身份判断。字段能不能当身份原因index不能列表一筛选、一排序就变name不建议同名菜谱或别名会冲突coverUrl不能图片可以替换不能代表业务对象recipeId可以详情、收藏、购物清单都能按它回到同一道菜这也是为什么我在ForEach里坚持用recipeId当 key。列表分页本身已经有时序问题如果 key 再不稳定页面复用会把问题放大看起来是滚动错位实际可能是组件复用了旧状态。以后怎么避免再踩新增列表功能先问自己什么新增排序排序字段是否稳定是否需要第二排序键新增筛选条件变化时是否重置游标和请求版本新增收藏刷新单条更新是否会破坏当前分页结果新增搜索联想搜索词变化是否会让旧分页结果回流新增推荐混排推荐结果和普通列表是否用同一套 recipeId 去重我现在更倾向于把分页逻辑写得啰嗦一点也不要把所有判断散在页面里。页面只负责触发“我要下一页”ViewModel 判断这次请求是否还有效Repository 负责按稳定边界查数据。三层职责清楚以后后面再加搜索、分类、推荐问题也能定位到具体层不会一出 bug 就只能整页重写。最后还有一个小经验分页问题一定要用连续操作压不要只测一次正常下拉。至少要测“快速触底两次、加载中切分类、加载中返回详情、收藏后继续加载、清空关键词再加载”。这些动作都过了才算分页状态真的稳。