卡片数据持久化——用 Preferences 让卡片“记住“用户选择
文章目录卡片数据持久化的场景为什么不用 AppStorage卡片 UI展示持久化数据FormAbility读写 Preferences 持久化数据Preferences 存储路径说明数据流完整图多卡片实例的持久化方案常见坑写在最后卡片进程会被系统随时回收State里的数据一断电就没了。但如果用户在卡片上做了选择比如选了某个主题、切换了显示模式希望下次打开还是之前的状态就需要持久化存储。StageServiceWidgetCards的widgetpersistentdata模块展示了这个方案核心是用Preferences存储卡片数据配合formProvider.updateForm刷新展示。卡片数据持久化的场景用户在卡片上勾选了某个状态如已完成重启手机后仍然显示已完成用户切换了卡片显示模式摘要/详细下次打开仍是详细模式卡片刷新失败后显示上次成功刷新的缓存数据而不是空白为什么不用 AppStorageAppStorage 只在应用运行期间存在进程被杀死后数据消失。Preferences 写入磁盘重启后还在这是根本区别。大白话版AppStorage 是内存里的便利贴Preferences 是写在硬盘里的笔记本。卡片 UI展示持久化数据// entry/src/main/ets/widgetpersistentdata/pages/WidgetPersistentDataCard.etsletstoragePersisnewLocalStorage();Entry(storagePersis)Componentstruct WidgetPersistentDataCard{readonlyFULL_WIDTH_PERCENT:string100%;readonlyFULL_HEIGHT_PERCENT:string100%;// list 是从 FormAbility 推送过来的持久化数据// 格式[{type: xxx}, ...]LocalStorageProp(list)list:Recordstring,string[][{type:a}];build(){Column(){Column(){// 显示持久化数据里的第一条记录的 type 字段Text((this.list[0][type])).fontColor(#FFFFFF).opacity(0.9).fontSize(14).margin({top:8%,left:10%})}.width(100%).alignItems(HorizontalAlign.Start)}.width(this.FULL_WIDTH_PERCENT).height(this.FULL_HEIGHT_PERCENT).backgroundImage($r(app.media.CardEvent)).backgroundImageSize(ImageSize.Cover)}}FormAbility读写 Preferences 持久化数据完整流程卡片创建时从 Preferences 读缓存数据 → 触发刷新时更新 Preferences 并推送给卡片// entry/src/main/ets/widgetpersistentdata/widgetabilify/WidgetPersistentDataFormAbility.etsimport{formBindingData,FormExtensionAbility,formProvider}fromkit.FormKit;import{Want}fromkit.AbilityKit;import{preferences}fromkit.ArkData;import{BusinessError}fromkit.BasicServicesKit;import{hilog}fromkit.PerformanceAnalysisKit;constTAGWidgetPersistentDataFormAbility;constDOMAIN_NUMBER0xFF00;constPREF_STORE_NAMEwidget_persistent_store;// Preferences 存储文件名constPREF_KEY_LISTwidget_list_data;// 存储的 keyexportdefaultclassWidgetPersistentDataFormAbilityextendsFormExtensionAbility{// 从 Preferences 读数据找不到就用默认值privateasyncreadDataFromPreferences():PromiseRecordstring,string[]{try{constprefsawaitpreferences.getPreferences(this.context,PREF_STORE_NAME);constrawDataawaitprefs.get(PREF_KEY_LIST,JSON.stringify([{type:a}]));returnJSON.parse(rawDataasstring)asRecordstring,string[];}catch(err){hilog.error(DOMAIN_NUMBER,TAG,读取 Preferences 失败:${JSON.stringify(err)});return[{type:a}];// 失败时返回默认值}}// 把新数据写入 PreferencesprivateasyncsaveDataToPreferences(data:Recordstring,string[]):Promisevoid{try{constprefsawaitpreferences.getPreferences(this.context,PREF_STORE_NAME);awaitprefs.put(PREF_KEY_LIST,JSON.stringify(data));awaitprefs.flush();// 必须调用 flush() 才会实际写入磁盘hilog.info(DOMAIN_NUMBER,TAG,数据持久化成功);}catch(err){hilog.error(DOMAIN_NUMBER,TAG,写入 Preferences 失败:${JSON.stringify(err)});}}onAddForm(want:Want):formBindingData.FormBindingData{hilog.info(DOMAIN_NUMBER,TAG,onAddForm);// 卡片创建时异步读取持久化数据并更新卡片constformIdJSON.stringify(want.parameters?.[ohos.extra.param.key.form_identity]??);// 先返回一个默认状态constdefaultData:Recordstring,Recordstring,string[]{list:[{type:loading...}]};// 异步加载持久化数据this.readDataFromPreferences().then(savedList{constformData:Recordstring,Recordstring,string[]{list:savedList};formProvider.updateForm(formId,formBindingData.createFormBindingData(formData)).catch((error:BusinessError){hilog.error(DOMAIN_NUMBER,TAG,加载持久化数据失败:${JSON.stringify(error)});});});returnformBindingData.createFormBindingData(defaultData);}onUpdateForm(formId:string):void{hilog.info(DOMAIN_NUMBER,TAG,onUpdateForm:${formId});// 模拟获取新数据实际项目里可能是网络请求constnewList:Recordstring,string[][{type:snow},// 从后端拿到的新数据];// 先持久化新数据this.saveDataToPreferences(newList).then((){// 再推送给卡片 UIconstformData:Recordstring,Recordstring,string[]{list:newList};returnformProvider.updateForm(formId,formBindingData.createFormBindingData(formData));}).catch((error:BusinessError){hilog.error(DOMAIN_NUMBER,TAG,更新卡片失败:${JSON.stringify(error)});});}onFormEvent(formId:string,message:string):void{hilog.info(DOMAIN_NUMBER,TAG,onFormEvent:${message});constmsg:Recordstring,stringJSON.parse(message);// 根据用户在卡片上的操作更新持久化状态if(msg.actionswitchType){constnewList:Recordstring,string[][{type:msg.value}];this.saveDataToPreferences(newList).then((){constformData:Recordstring,Recordstring,string[]{list:newList};returnformProvider.updateForm(formId,formBindingData.createFormBindingData(formData));});}}}Preferences 存储路径说明调用preferences.getPreferences(context, storeName)时storeName决定了文件位置传文件名如widget_store文件存在应用私有目录/data/storage/el2/base/haps/entry/preferences/widget_store传绝对路径如/data/storage/el2/base/haps/form_store直接用这个路径在 FormExtensionAbility 里推荐用绝对路径因为 FormAbility 的上下文路径可能和主 UIAbility 不同// 推荐写法用绝对路径跨进程一致constDATA_STORAGE_PATH/data/storage/el2/base/haps/form_store;constprefsawaitpreferences.getPreferences(this.context,DATA_STORAGE_PATH);数据流完整图多卡片实例的持久化方案一个应用可以被用户添加多个卡片实例每个都有独立的 formId。如果每个实例的数据不同存储时要用 formId 作为 key// 以 formId 为 key 存储不同实例的数据privateasyncsaveFormData(formId:string,data:Recordstring,string[]):Promisevoid{constprefsawaitpreferences.getPreferences(this.context,/data/storage/el2/base/haps/form_store);// 每个 formId 对应一条存储记录awaitprefs.put(formId,JSON.stringify(data));awaitprefs.flush();}// 删除卡片时清理对应数据onRemoveForm(formId:string):void{preferences.getPreferences(this.context,/data/storage/el2/base/haps/form_store).then(prefs{prefs.delete(formId).then(()prefs.flush());});}常见坑坑1忘了调flush()prefs.put()只是写到内存缓冲区不调flush()数据不会落磁盘进程被杀之后数据丢失。// 数据可能丢失awaitprefs.put(key,value);// 确保落磁盘awaitprefs.put(key,value);awaitprefs.flush();// 必须坑2存复杂对象要 JSON.stringifyPreferences 只支持基本类型string/number/boolean/Uint8Array/bigint。存数组或对象要先JSON.stringify取出来要JSON.parse。坑3FormExtensionAbility 里的存储路径和 UIAbility 不一样两个 Ability 的this.context.filesDir路径不同用相对路径会在不同的目录建文件。用绝对路径能保证一致性。写在最后卡片的持久化存储是一个容易被忽视但一定会踩坑的点。只要记住进程可能随时被杀不持久化的数据就是假设自己永远活着。开发时加上 Preferences 存储代价很小但能给用户省去很多困惑“我明明设置了为啥又回到默认了”。