ArkTS 购物清单勾选后刷新又丢了怎么办:中式美食 checked 状态怎么落到 RDB

发布时间:2026/7/12 8:58:27
ArkTS 购物清单勾选后刷新又丢了怎么办:中式美食 checked 状态怎么落到 RDB 用户打开买菜清单先勾掉“清蒸鲈鱼”“排骨”“时蔬”进度显示 3/7。这个体验看起来很小但如果用户切到菜谱详情、返回首页、再打开购物清单勾选状态突然没了用户会直接觉得应用不可靠。中式美食当前购物清单已经有勾选、统计和分享入口后面做 HarmonyOS 版本时不能把 checked 状态只留在页面内存里至少要明确哪些状态可以临时放 ViewModel哪些状态必须写进 RDB。应用名称中式美食这篇解决什么购物清单勾选后刷新、返回、重进页面时状态丢失谁会搜这篇做 HarmonyOS 买菜清单、任务列表、待办勾选、离线状态的开发者点进来能拿到什么checked 字段设计、RDB 更新逻辑、ViewModel 状态边界和验收用例当前项目证据ShoppingListState、ShoppingListViewModel、remainingItems、checkedCountHarmonyOS 目标能力ArkTS、ArkUI、relationalStore、RdbPredicates、CREATE INDEX本章导读这篇只讲一件事买菜清单勾选状态不能乱。购物清单不像普通展示列表它会被用户边走边点状态变化频繁而且用户很可能离开页面再回来。如果只靠页面数组保存短流程能跑真实使用就容易丢。章节重点当前代码暴露的问题为什么内存里的checked只能解决当前页面字段怎么设计checked、checkedAt、updatedAt各自负责什么RDB 怎么更新单条勾选要小而稳定不要整表重写ArkUI 怎么展示页面只拿结果不自己推断统计怎么验收刷新、返回、异常、重复点击都要测当前验证环境和技术栈项目说明DevEco Studio6.x 系列HarmonyOS SDKAPI 12/ArkTS 工程UI 层ArkUI 声明式页面状态层ViewModel 暴露可渲染状态数据层ohos.data.relationalStore、RDB、RdbPredicates当前项目参考中式美食购物清单模块、餐桌方案模块、分享/提醒入口当前项目里购物清单状态已经能表达“总数、已勾选数量、剩余项”。这说明页面模型方向没问题。真正要补的是持久化边界。classShoppingListItem{constShoppingListItem({requiredthis.name,this.checkedfalse,});finalStringname;finalbool checked;}classShoppingListState{constShoppingListState({requiredthis.plan,requiredthis.items,});intgetcheckedCountitems.where((item)item.checked).length;intgettotalCountitems.length;StringgetprogressText$checkedCount/$totalCount;ListShoppingListItemgetremainingItemsitems.where((item)!item.checked).toList();}这段代码作为页面状态是可以的但如果它是唯一数据源就会有风险页面销毁后状态没了跨页面返回后状态可能重建分享和提醒拿到的也可能不是最新值。先把状态分成两类不是所有状态都要立刻进数据库。中式美食购物清单里我会先分成“页面临时状态”和“业务事实状态”。状态示例放哪里原因页面加载中loadingtrueViewModel只影响当前页面当前错误提示errorMessageViewModel下次进入可以重新请求当前筛选条件只看未买、只看蔬菜ViewModel 或 Preferences看是否需要恢复食材是否已买checkedRDB用户行为事实不能丢勾选时间checkedAtRDB后面可用于排序和记录清单所属方案planIdRDB跨页面、跨入口都要查我会把“用户已经确认买过某个食材”视为业务事实。这个状态和主题色、临时空态不一样它影响下一次打开页面看到什么也影响分享清单和提醒。RDB 表结构要保留勾选事实购物清单表不需要一开始设计得很重但几个字段不能省id、plan_id、display_name、normalized_name、checked、checked_at、updated_at。constCREATE_SHOPPING_ITEM_TABLECREATE TABLE IF NOT EXISTS shopping_item ( id TEXT PRIMARY KEY, plan_id TEXT NOT NULL, recipe_id TEXT, display_name TEXT NOT NULL, normalized_name TEXT NOT NULL, category TEXT, amount_text TEXT, checked INTEGER DEFAULT 0, checked_at INTEGER, updated_at INTEGER NOT NULL );字段用途说明id列表稳定 keyArkUIForEach不能用 index 顶替plan_id所属餐桌方案回到某个方案时能恢复清单display_name给用户看保留熟悉叫法normalized_name合并和搜索避免番茄/西红柿拆两条checked是否已买需要跨页面保留checked_at勾选时间后面可做最近确认排序updated_at最近修改时间用于冲突处理和刷新索引也要服务真实入口。购物清单最常见的查询是某个方案下按未买/已买状态取列表。constCREATE_SHOPPING_INDEXES[CREATE INDEX IF NOT EXISTS idx_shopping_plan_checked ON shopping_item(plan_id, checked, updated_at DESC),CREATE INDEX IF NOT EXISTS idx_shopping_plan_name ON shopping_item(plan_id, normalized_name)];不要一开始给所有字段都建索引。购物清单写入频率比菜谱详情高索引越多更新越重。先保证高频入口稳定。勾选更新要小不要整表重写用户点一条食材最好的更新方式是只改这一条。不要因为一个勾选动作把整张购物清单重新写一遍。整表重写会让失败恢复、并发点击和后续同步都变复杂。importrelationalStorefromohos.data.relationalStore;exportclassShoppingListRepository{constructor(privatereadonlystore:relationalStore.RdbStore){}asynctoggleChecked(itemId:string,checked:boolean):Promisevoid{constvalues:relationalStore.ValuesBucket{checked:checked?1:0,checked_at:checked?Date.now():null,updated_at:Date.now()};constpredicatesnewrelationalStore.RdbPredicates(shopping_item);predicates.equalTo(id,itemId);awaitthis.store.update(values,predicates);}}这里有两个细节细节为什么checked用 0/1RDB 里更稳定查询也直接checked_at只在勾选时写取消勾选后不应该保留旧确认时间updated_at每次都写后续排查和同步需要知道最后修改时间按id更新避免同名食材误改查询结果要直接给页面用ArkUI 页面不应该自己算“已买数量”和“剩余数量”的业务规则。ViewModel 可以算但它的输入应该是 Repository 返回的干净数据。exportinterfaceShoppingListItemModel{id:string;title:string;category:string;amountText:string;checked:boolean;}exportinterfaceShoppingListPageState{items:ShoppingListItemModel[];totalCount:number;checkedCount:number;remainingCount:number;}Repository 查询时直接按plan_id拿当前方案的清单。asyncfunctionqueryShoppingList(store:relationalStore.RdbStore,planId:string):PromiseShoppingListItemModel[]{constpredicatesnewrelationalStore.RdbPredicates(shopping_item);predicates.equalTo(plan_id,planId);predicates.orderByAsc(checked);predicates.orderByDesc(updated_at);constresultSetawaitstore.query(predicates,[id,display_name,category,amount_text,checked]);constitems:ShoppingListItemModel[][];while(resultSet.goToNextRow()){items.push({id:resultSet.getString(resultSet.getColumnIndex(id)),title:resultSet.getString(resultSet.getColumnIndex(display_name)),category:resultSet.getString(resultSet.getColumnIndex(category)),amountText:resultSet.getString(resultSet.getColumnIndex(amount_text)),checked:resultSet.getLong(resultSet.getColumnIndex(checked))1});}resultSet.close();returnitems;}页面拿到的就是能渲染的数据不需要再猜每个字段什么意思。ViewModel 做乐观更新但失败要回滚购物清单勾选是高频操作点一下等数据库写完再刷新用户会觉得慢。可以做乐观更新先让页面变再写 RDB如果写失败再回滚并提示。ObservedexportclassShoppingListViewModel{items:ShoppingListItemModel[][];errorText:string;constructor(privatereadonlyrepository:ShoppingListRepository){}asynctoggle(itemId:string){constindexthis.items.findIndex(itemitem.iditemId);if(index0){return;}constoldItemthis.items[index];constnextChecked!oldItem.checked;this.items[index]{...oldItem,checked:nextChecked};try{awaitthis.repository.toggleChecked(itemId,nextChecked);}catch(error){this.items[index]oldItem;this.errorText这条食材刚才没保存成功可以再点一次;}}}乐观更新不是“假装成功”。它必须有回滚。否则数据库写失败后页面看起来是已买下一次打开又变回来用户会更困惑。情况页面怎么处理数据怎么处理写入成功保持新状态RDB 已更新写入失败回滚旧状态不改 RDB连续点击只保留最后一次动作用更新时间或请求序号兜底页面返回重新读 RDB不信旧内存分享清单从最新状态生成不用过期列表ArkUI 列表 key 不能用 index勾选状态最怕列表复用错位。如果用 index 当 key筛选、排序、删除后上一行的状态可能跑到下一行。Componentstruct ShoppingList{ObjectLinkvm:ShoppingListViewModel;build(){List(){ForEach(this.vm.items,(item:ShoppingListItemModel){ListItem(){Row(){Checkbox({name:item.id,group:shopping}).select(item.checked).onChange(()this.vm.toggle(item.id))Text(item.title).fontSize(16).decoration({type:item.checked?TextDecorationType.LineThrough:TextDecorationType.None})}}},(item:ShoppingListItemModel)item.id)}}}这一点很基础但很容易被忽略。买菜清单经常按“未买优先”排序勾选后列表顺序可能变化业务 id 是最稳的 key。工程验收验收项操作通过标准首次打开进入购物清单RDB 读出列表统计正确勾选一项点“莲藕”已买数量 1列表状态立刻变化刷新页面退出再进购物清单勾选状态仍在返回详情从食材点进菜谱再返回原购物清单状态不丢写入失败模拟 RDB 更新异常页面回滚并给出可理解提示连续点击快速点同一条两次最终状态和最后一次点击一致只看未买打开未买筛选checked0的食材才出现可以用一个简单检查把核心逻辑压住asyncfunctionverifyToggle(repository:ShoppingListRepository,itemId:string){awaitrepository.toggleChecked(itemId,true);constafterCheckedawaitrepository.getItem(itemId);if(!afterChecked.checked){thrownewError(勾选后 RDB 没有保存 checked1);}awaitrepository.toggleChecked(itemId,false);constafterCancelawaitrepository.getItem(itemId);if(afterCancel.checked){thrownewError(取消勾选后 RDB 没有恢复 checked0);}}常见问题和边界问题我的判断checked 状态能不能只放 Preferences不建议购物清单是多条业务数据不是单个开关能不能每次都整表覆盖不建议失败恢复和并发点击会变复杂勾选后要不要立刻排序可以但要保证 key 稳定不要让用户点错行checkedAt 有没有必要有后面做最近购买、家庭协作、同步冲突会用到分享清单从哪里取从最新 ViewModel 状态或重新查 RDB不要用旧缓存本章小结中式美食购物清单的勾选状态不是一个小小的 UI 勾选框问题。它背后是用户行为事实这个食材买过了还是没买。只要这个事实会影响下一次打开、分享清单、提醒和家庭协作就不应该只留在页面内存里。我的处理方式是页面可以先乐观更新让用户感觉顺Repository 必须把 checked 写进 RDB失败时 ViewModel 要回滚列表 key 必须用业务 id。这样做并不复杂但能避开很多真实使用里的小崩溃刷新丢状态、返回后进度不准、筛选后点错行、分享出去的清单不是最新。如果你也在做 HarmonyOS 的待办、清单、菜谱、购物或任务类页面可以先问一句这个勾选是“当前页面临时看一下”还是“用户已经完成了一个动作”。前者放 ViewModel后者就应该认真落到本地数据里。