Preferences 和 RDB 数据不一致怎么办:收藏、最近浏览和购物清单怎么对账
2026/7/13 2:12:10 网站建设 项目流程

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):Promise<void>{constids=awaitpreferenceStore.getFavoriteIds()constnextIds=favorite?[...ids,recipeId]:ids.filter(id=>id!==recipeId)awaitpreferenceStore.setFavoriteIds(nextIds)}

这个写法在 Demo 里没问题,在真实项目里会出事。收藏不只是一个按钮状态,后面还会影响列表筛选、推荐排序、详情页入口、用户行为分析。只存在 Preferences 里,后面很难查。

正确做法:事实进 RDB,快速入口进 Preferences

收藏这类状态,我会让 RDB 做事实来源。Preferences 可以保留最近几个 id,用来优化首屏或恢复入口,但不能反过来当最终数据。

exportclassFavoriteRepository{asyncsetFavorite(recipeId:string,favorite:boolean):Promise<void>{if(favorite){awaitthis.rdbStore.insert('favorite_recipe',{recipe_id:recipeId,created_at:Date.now()})return}constpredicates=newrelationalStore.RdbPredicates('favorite_recipe')predicates.equalTo('recipe_id',recipeId)awaitthis.rdbStore.delete(predicates)}asyncisFavorite(recipeId:string):Promise<boolean>{constpredicates=newrelationalStore.RdbPredicates('favorite_recipe')predicates.equalTo('recipe_id',recipeId)returnawaitthis.exists(predicates)}}

页面要快,可以在 ViewModel 里先做乐观更新,但最终要以 Repository 的结果为准。

exportclassRecipeDetailViewModel{favorite=falseasynctoggleFavorite(recipeId:string):Promise<void>{constnext=!this.favoritethis.favorite=nexttry{awaitthis.favoriteRepository.setFavorite(recipeId,next)awaitthis.preferenceStore.touchRecentFavorite(recipeId)}catch(err){this.favorite=!nextthrowerr}}}

这段逻辑的关键不是“写了两处”,而是两处的职责不同:RDB 负责事实,Preferences 负责快速入口和体验优化。写失败时要回滚页面状态,不然用户会以为收藏成功了。

最近浏览可以先轻后重

最近浏览的场景不一样。用户进详情页时,首页可能马上要展示“刚看过”。这里可以先写 Preferences,再异步补一条 RDB 行为明细。

exportclassViewedRecipeService{asyncrecordViewed(recipeId:string):Promise<void>{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):Promise<void>{constpredicates=newrelationalStore.RdbPredicates('shopping_ingredient')predicates.equalTo('row_id',rowId)awaitthis.rdbStore.update({checked:checked?1:0,updated_at:Date.now()},predicates)}asyncqueryCurrentList():Promise<ShoppingIngredientRow[]>{constpredicates=newrelationalStore.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():Promise<void>{constids=awaitthis.preferenceStore.getRecentViewedIds()constexistingIds=awaitthis.recipeRepository.filterExistingRecipeIds(ids)if(existingIds.length!==ids.length){awaitthis.preferenceStore.setRecentViewedIds(existingIds)}}asyncreconcileFavoriteFastEntry():Promise<void>{constfastIds=awaitthis.preferenceStore.getRecentFavoriteIds()constvalidIds=awaitthis.favoriteRepository.filterFavoriteIds(fastIds)awaitthis.preferenceStore.setRecentFavoriteIds(validIds)}}

这样做的好处是,页面不会长期带着脏状态跑。即便某次写入失败,下一次进入关键页面也能把状态拉回正确轨道。

回归验证怎么做

步骤操作预期
1收藏一道菜,返回列表列表收藏状态立即亮起
2关闭应用再打开收藏页收藏仍然存在
3进入三道菜详情页首页最近浏览顺序正确
4删除或隐藏一条菜谱后重启最近浏览自动清掉无效 id
5勾选购物清单食材后刷新checked 状态从 RDB 回填
6切换筛选条件再返回统计数量和明细一致

本章小结

Preferences 和 RDB 不要互相抢工作。我的判断标准是:

  • 需要查、排、关联、统计的,放 RDB。
  • 只是恢复入口、加快首屏、保存轻量偏好的,放 Preferences。
  • 用户明确动作,比如收藏和购物清单 checked,要能落到事实数据里。
  • 页面显示和数据层不一致时,要以事实来源为准做对账。

中式美食后面还会继续加收藏、最近浏览、购物清单和推荐排序,这些功能只要进入同一个本地数据体系,就必须先把边界讲清楚。否则文章里看起来每个模块都能跑,真实用户一来回切页面,状态就会开始互相打架。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询