ArkData Preferences:文搜图筛选快照版本迁移与坏值回退【鸿蒙心迹】
一款文搜图应用,最容易被低估的不是搜索按钮,而是用户第二次进入页面时,还能不能继续昨天那次筛选。关键词、排序方式与仅看本地的开关都属于轻量配置,看起来放到 Preferences 里就结束了。真正进入持续迭代阶段,问题却不在“能否存下一个字符串”,而在版本变更后的旧数据还能不能解释。
本文使用 QueryShelf 作为说明性 Demo。任务编号统一为 QF-276;示例旧快照是 schema=1、关键词“雨夜天桥”、排序 recent、仅本地 true,新结构使用 schema=2 与 timeDesc。页面展示“已恢复”和“24 条演示结果”,其中结果数只是固定的 UI 样本,绝不代表真实图库查询或检索性能。

一、问题藏在升级后的第一次恢复里
1. 筛选状态和检索结果必须分家
搜索条件是用户意图,搜索结果是某个时刻的数据快照。假如把二者一起塞进首选项,重新打开页面就可能把过期图片、旧权限状态和临时 URI 当成可复用结果。本文只持久化三个业务字段及一个结构版本:关键词、排序键、仅本地开关、schema。图片列表始终由业务层重新构建,演示里的 24 条也不写入 Preferences。
需要控制的入口共有三种:正常退出后再次打开;应用升级后读取上一个版本的快照;持久化数据损坏或字段格式错误。第一个入口验证读写时序,第二个验证迁移规则,第三个验证回退与错误提示。这三个分支若混在一个 catch 里,很容易把升级逻辑失败伪装成“用户从没筛选过”。
2. 一份字段契约比页面初值更重要
先固定业务约定:recent 是旧版排序代码,新版用 timeDesc 表示时间倒序;无法识别的排序键回退到 timeDesc。关键词最大保留 40 个字符,避免旧值无限放大输入框。仅本地开关只接受布尔值,不做字符串 "true" 的宽松转换。这样,迁移既可解释,也能在单元逻辑演练中稳定复现。
二、让读入操作先通过类型和版本检查
这里的第一段代码解决坏 JSON、字段缺失和旧排序值三类问题。它不调用系统接口,只把一个字符串变成安全的、可渲染的业务对象。代码放在 model/FilterSnapshot.ets;schema 是项目自定义结构版本,并非 HarmonyOS SDK 版本。
export interface FilterSnapshot {
schema: number;
keyword: string;
sort: string;
onlyLocal: boolean;
}
export const DEFAULT_FILTER: FilterSnapshot = {
schema: 2, keyword: '', sort: 'timeDesc', onlyLocal: true
};
export function decodeSnapshot(raw: string): FilterSnapshot {
if (raw.length === 0 || raw.length > 2048) return DEFAULT_FILTER;
try {
const data = JSON.parse(raw) as FilterSnapshot;
if (!data || typeof data.keyword !== 'string' ||
typeof data.sort !== 'string' ||
typeof data.onlyLocal !== 'boolean') return DEFAULT_FILTER;
if (data.schema !== 1 && data.schema !== 2) return DEFAULT_FILTER;
const sort = data.sort === 'relevance' ? 'relevance' : 'timeDesc';
return { schema: 2, keyword: data.keyword.trim().slice(0, 40),
sort: sort, onlyLocal: data.onlyLocal };
} catch (_) {
return DEFAULT_FILTER;
}
}
代码明确规定了一条单向路径:旧版 recent 迁移为 timeDesc,合法的 relevance 保留,其余排序字段走默认值。它没有对损坏 JSON 做“猜测性修复”。默认对象只是恢复页面的安全起点,不代表数据已经成功读盘。生产环境应另外返回 recovered、migrated 等诊断标志,而不让 UI 仅凭结果结构判断磁盘是否健康。
有一个特别容易忽略的边界:JSON.parse 成功只说明文本有合法语法,不保证数据符合我们的协议。因此需要继续检查字段类型与 schema。即使做了这些检查,也不能把首选项当成防篡改的安全仓库;它适合轻量配置,而不适合账户密钥、鉴权结果或重要业务凭证。
三、Preferences 负责落盘,业务层负责迁移
1. 只存一份完整快照
第二段代码写入 service/FilterStore.ets,处理持久化层。官方 ArkData 首选项接口提供 getPreferences、get、put 和 flush;put 修改缓存,XML 存储模式下需要 flush 将缓存写入持久化文件。这里把多个字段序列化为同一个 snapshot 键,可以减少业务层发生字段版本错配的机会,但并不把它宣称为数据库事务。
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
import { FilterSnapshot, decodeSnapshot } from '../model/FilterSnapshot';
export class FilterStore {
async load(ctx: common.UIAbilityContext): Promise<FilterSnapshot> {
const db = await preferences.getPreferences(ctx, { name: 'query_shelf' });
const value = await db.get('snapshot', '');
const raw = typeof value === 'string' ? value : '';
return decodeSnapshot(raw);
}
async save(ctx: common.UIAbilityContext,
data: FilterSnapshot): Promise<void> {
const db = await preferences.getPreferences(ctx, { name: 'query_shelf' });
await db.put('snapshot', JSON.stringify(data));
await db.flush();
}
}
这段代码故意把读盘和落盘写成两个入口。读取失败要由页面显示恢复失败,并允许用户继续编辑;写入失败则不能提示“保存成功”。实际项目可以为 load 增加带状态的返回对象,把“空值”“格式错误”“兼容迁移”区分开。尤其要记住 Preferences 的实例存在缓存:同进程第二次 getPreferences 不意味着重新扫描文件,不能拿它模拟进程重启测试。
这里也不使用多进程同时写同一个首选项文件。华为文档明确提醒:Preferences 不能保证跨进程并发安全,可能出现文件损坏或数据丢失。若产品需要并发编辑、冲突合并或批量数据事务,就应该另外设计数据层,而不是把 flush() 当成通用同步屏障。
2. 页面恢复与正在输入的用户存在竞争
第三段代码关注“晚回来”的恢复结果。假设页面刚打开便异步读取旧快照,而用户已经输入新词;此时恢复回调不能把新的输入覆盖回“雨夜天桥”。页面通过自定义序号 restoreSeq 拦截过期结果。下面为 pages/FilterPage.ets 中的核心片段,组件其余布局采用常规 TextInput 与按钮即可。
import { common } from '@kit.AbilityKit';
import { FilterStore } from '../service/FilterStore';
@Entry
@Component
struct FilterPage {
@State keyword: string = '';
@State sort: string = 'timeDesc';
@State status: string = '待恢复';
private restoreSeq: number = 0;
private store: FilterStore = new FilterStore();
aboutToAppear(): void { void this.restoreFilter(); }
aboutToDisappear(): void { this.restoreSeq++; }
private async restoreFilter(): Promise<void> {
const seq = ++this.restoreSeq;
try {
const data = await this.store.load(getContext(this) as common.UIAbilityContext);
if (seq !== this.restoreSeq) return;
this.keyword = data.keyword;
this.sort = data.sort;
this.status = '已恢复';
} catch (_) {
if (seq === this.restoreSeq) this.status = '恢复失败';
}
}
build() {
Column() {
TextInput({ text: this.keyword, placeholder: '筛选关键词' })
.onChange((value: string) => {
this.restoreSeq++; this.keyword = value;
});
Text('筛选状态:' + this.status);
Text('排序:' + this.sort);
}.padding(16)
}
}
restoreSeq 是业务自定义递增数字,并非系统取消令牌。它只能挡住当前页面的过期状态覆盖,并不会中断 Preferences 底层 I/O。页面离开时序号递增,能让已经发出的 Promise 结果失去修改 UI 的资格。若页面需要同时保存,应该独立建立“保存代次”,不要混用读取代次来表示落盘完成。

四、把故障路径写成可检查的演示协议
QueryShelf 的演示输入始终固定:任务 QF-276,旧结构 1,新结构 2,关键词“雨夜天桥”,排序 recent→timeDesc,onlyLocal=true。示意开发界面的 HiLog 使用 QueryShelf QF-276 migrate 1->2 sort=recent=>timeDesc 与 QueryShelf QF-276 restore query=雨夜天桥 count=24 作为预设演示日志。它们是设计时的观察点,并非从真实设备采集。
建议把检查拆为四组。正常快照应保留关键词和开关;旧结构应按映射规则升级;损坏 JSON 应使用安全默认值并提供可见提示;用户修改输入后才返回的异步恢复结果应被丢弃。测试时务必让每个用例从独立数据起点开始,防止上一个用例留在 Preferences 缓存里的值干扰当前结论。
手机示意图显示 24 条图片占位结果,是为了把筛选条件和业务结果分开展示。它不是文搜图模型的准确率证明,检索数也不是性能数据。实现真实文搜图时,搜索索引仍应根据已授权的图库范围重新查询,再将 UI 状态与当前请求关联;不能由于“已恢复”就默认所有图片仍可访问。

五、真正值得交付的结论
这套方案的价值不在多存了一个 schema 数字,而在保存、读取、迁移、回退都能说清楚自己的边界。对于轻量配置,用 Preferences 的单键快照配合严格解码足够直接;对于大量图片记录、跨进程并发和复杂一致性需求,首选项不是合适的数据模型。
正式接入前还要在对应 SDK 与设备形态上做编译和生命周期验证,尤其是 getContext 获取的实际上下文、卸载重装之后的数据行为、异常写入后的文件恢复以及 UI 输入并发。本文没有进行这些设备验证,不将演示界面写成真实测试结论。
最后一个容易漏掉的验收点是错误是否会被‘下一次成功’掩盖:例如一次落盘失败,页面不应该立即展示保存成功的绿色状态,也不应继续把这份内存数据当作下次能够恢复的凭证。建议把保存按钮的禁用、忙碌状态、失败重试和离开提醒分别设计,待 flush 完成后才反馈已保存。对于用户主动清空筛选条件的情况,应写入合法的空关键词快照,而不是删除整个偏好文件,以免误伤其他配置。这个小区别能让恢复行为更可解释,也便于后续做版本升级回归。
资料依据:华为 HarmonyOS 官方《@ohos.data.preferences(用户首选项)》API 参考,更新时间 2026-09-09:https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-data-preferences。图 01~03 均为示意素材,非真机截图、真实检索日志或审核证据。
更多推荐

所有评论(0)