Preferences 和 RDB 数据不一致怎么办:收藏、最近浏览和购物清单怎么对账

📅 2026/7/13 2:12:13
Preferences 和 RDB 数据不一致怎么办:收藏、最近浏览和购物清单怎么对账
Preferences 和 RDB 数据不一致怎么办收藏、最近浏览和购物清单怎么对账中式美食里有几类状态特别容易写乱用户刚收藏了一道菜回到列表没亮刚浏览过的菜首页“最近看过”没出现购物清单里勾选了食材刷新后又回到旧状态。表面看是页面刷新问题本质通常是 Preferences 和 RDB 的边界没分清。我的处理方式不是把所有数据都塞进一个地方而是先分工Preferences 放轻量、快速、可以重建的状态RDB 放需要查询、排序、回溯的明细。然后再做一层对账让页面状态能从数据层解释清楚。本文导读问题处理方式收藏后列表状态不亮收藏关系以 RDB 为准Preferences 只做入口缓存最近浏览和详情页不一致最近浏览先写 Preferences再异步补 RDB 明细购物清单 checked 状态刷新后错明细 checked 写 RDB页面从查询结果回填怎么判断两边没有写乱建一个对账方法启动和返回时做轻量校验环境和文件边界项目内容开发工具DevEco Studio技术栈HarmonyOS Stage 模型、ArkTS、ArkUI快速状态entry/src/main/ets/data/local/AppPreferenceStore.ets明细数据entry/src/main/ets/data/repository/FavoriteRepository.ets购物清单entry/src/main/ets/data/repository/ShoppingListRepository.ets页面入口entry/src/main/ets/features/home/HomeViewModel.ets先分清Preferences 不是小型数据库Preferences 很适合放“轻状态”最后一次筛选条件、最近打开的 tab、最近浏览 id 列表、首屏缓存标记。它的优势是快读写也简单。但它不适合承担复杂查询也不适合当唯一事实来源。数据放哪里原因最近浏览 recipeId 列表Preferences数量少读得快丢了也能从 RDB 或行为日志重建收藏关系RDB要查询、排序、统计、和笔记/推荐关联购物清单 checkedRDB用户动作要可靠保存刷新和重启都不能丢当前筛选条件Preferences 或页面状态只是恢复入口不是业务事实一个容易出错的写法最常见的问题是收藏时只改 Preferences页面看起来很快但 RDB 没同步。过一会儿页面重新从 RDB 查状态就变回去了。// 不建议只改快速状态明细库没有事实记录asyncfunctiontoggleFavorite(recipeId:string,favorite:boolean):Promisevoid{constidsawaitpreferenceStore.getFavoriteIds()constnextIdsfavorite?[...ids,recipeId]:ids.filter(idid!recipeId)awaitpreferenceStore.setFavoriteIds(nextIds)}这个写法在 Demo 里没问题在真实项目里会出事。收藏不只是一个按钮状态后面还会影响列表筛选、推荐排序、详情页入口、用户行为分析。只存在 Preferences 里后面很难查。正确做法事实进 RDB快速入口进 Preferences收藏这类状态我会让 RDB 做事实来源。Preferences 可以保留最近几个 id用来优化首屏或恢复入口但不能反过来当最终数据。exportclassFavoriteRepository{asyncsetFavorite(recipeId:string,favorite:boolean):Promisevoid{if(favorite){awaitthis.rdbStore.insert(favorite_recipe,{recipe_id:recipeId,created_at:Date.now()})return}constpredicatesnewrelationalStore.RdbPredicates(favorite_recipe)predicates.equalTo(recipe_id,recipeId)awaitthis.rdbStore.delete(predicates)}asyncisFavorite(recipeId:string):Promiseboolean{constpredicatesnewrelationalStore.RdbPredicates(favorite_recipe)predicates.equalTo(recipe_id,recipeId)returnawaitthis.exists(predicates)}}页面要快可以在 ViewModel 里先做乐观更新但最终要以 Repository 的结果为准。exportclassRecipeDetailViewModel{favoritefalseasynctoggleFavorite(recipeId:string):Promisevoid{constnext!this.favoritethis.favoritenexttry{awaitthis.favoriteRepository.setFavorite(recipeId,next)awaitthis.preferenceStore.touchRecentFavorite(recipeId)}catch(err){this.favorite!nextthrowerr}}}这段逻辑的关键不是“写了两处”而是两处的职责不同RDB 负责事实Preferences 负责快速入口和体验优化。写失败时要回滚页面状态不然用户会以为收藏成功了。最近浏览可以先轻后重最近浏览的场景不一样。用户进详情页时首页可能马上要展示“刚看过”。这里可以先写 Preferences再异步补一条 RDB 行为明细。exportclassViewedRecipeService{asyncrecordViewed(recipeId:string):Promisevoid{awaitthis.preferenceStore.prependRecentViewed(recipeId,20)try{awaitthis.viewedRepository.insertViewed({recipeId,viewedAt:Date.now()})}catch(err){// 行为明细失败不阻塞用户浏览但要留下日志方便后续排查hilog.warn(0x0000,ViewedRecipeService,record viewed failed:${JSON.stringify(err)})}}}这里我接受“RDB 行为明细偶尔晚一点”因为最近浏览不是强一致交易。但收藏、购物清单不一样它们是用户明确动作不能随便丢。购物清单 checked 要按明细回填购物清单的 checked 状态更接近任务进度。用户勾选“番茄”刷新后还应该是番茄已勾选。这个状态要写回 RDB页面每次加载时从查询结果回填。exportclassShoppingListRepository{asyncupdateChecked(rowId:string,checked:boolean):Promisevoid{constpredicatesnewrelationalStore.RdbPredicates(shopping_ingredient)predicates.equalTo(row_id,rowId)awaitthis.rdbStore.update({checked:checked?1:0,updated_at:Date.now()},predicates)}asyncqueryCurrentList():PromiseShoppingIngredientRow[]{constpredicatesnewrelationalStore.RdbPredicates(shopping_ingredient)predicates.orderByAsc(checked)predicates.orderByAsc(display_name)returnthis.queryRows(predicates)}}如果业务要按食材合并就把rowId换成稳定的ingredientKey。但边界必须说清楚不能一会儿按行一会儿按食材。做一个轻量对账方法对账不是每天扫全库而是在关键节点做小范围校验。比如启动首页时检查最近浏览 id 是否还存在进入收藏页时检查 Preferences 里的快速 id 是否和 RDB 收藏表矛盾进入购物清单时检查 checked 数量是否能从明细查出来。对账点检查什么发现问题怎么处理首页启动最近浏览 id 是否还能查到菜谱清掉无效 id收藏页打开Preferences 快速 id 是否仍在收藏表以 RDB 为准刷新详情页返回当前 recipeId 收藏状态是否一致重新查 RDB 并回写页面购物清单打开checked 统计是否等于明细查询结果以明细表重新计算exportclassLocalStateReconcileService{asyncreconcileRecentViewed():Promisevoid{constidsawaitthis.preferenceStore.getRecentViewedIds()constexistingIdsawaitthis.recipeRepository.filterExistingRecipeIds(ids)if(existingIds.length!ids.length){awaitthis.preferenceStore.setRecentViewedIds(existingIds)}}asyncreconcileFavoriteFastEntry():Promisevoid{constfastIdsawaitthis.preferenceStore.getRecentFavoriteIds()constvalidIdsawaitthis.favoriteRepository.filterFavoriteIds(fastIds)awaitthis.preferenceStore.setRecentFavoriteIds(validIds)}}这样做的好处是页面不会长期带着脏状态跑。即便某次写入失败下一次进入关键页面也能把状态拉回正确轨道。回归验证怎么做步骤操作预期1收藏一道菜返回列表列表收藏状态立即亮起2关闭应用再打开收藏页收藏仍然存在3进入三道菜详情页首页最近浏览顺序正确4删除或隐藏一条菜谱后重启最近浏览自动清掉无效 id5勾选购物清单食材后刷新checked 状态从 RDB 回填6切换筛选条件再返回统计数量和明细一致本章小结Preferences 和 RDB 不要互相抢工作。我的判断标准是需要查、排、关联、统计的放 RDB。只是恢复入口、加快首屏、保存轻量偏好的放 Preferences。用户明确动作比如收藏和购物清单 checked要能落到事实数据里。页面显示和数据层不一致时要以事实来源为准做对账。中式美食后面还会继续加收藏、最近浏览、购物清单和推荐排序这些功能只要进入同一个本地数据体系就必须先把边界讲清楚。否则文章里看起来每个模块都能跑真实用户一来回切页面状态就会开始互相打架。