山海万灵 HarmonyOS 文化知识实战(16):发现记录与探索位置的 Preferences 持久化
一、把发现进度保存成可恢复的事实
在山海万灵的离线浏览链路里,读者会连续完成图鉴发现、区域切换和展厅浏览。应用重启后仍需回到同一段探索,而不是重新从一张空白图鉴开始。持久化层保存的对象因此不是页面快照,而是一组能够重新计算界面的最小事实:已发现神兽 ID、当前区域、当前展厅、数据版本和更新时间。
interface ShanhaiLocalProgress {
schemaVersion: number
discoveredIds: string[]
selectedRegionId: string
selectedHallId: string
updatedAt: number
}
这组字段都由用户动作直接产生。区域百分比、展厅进度和护照印章则由图鉴目录与发现 ID 在读取阶段计算得到。这样能把输入事实和显示结果分开管理。
二、Preferences 的读写边界
Preferences 存储使用固定名称保存同一份本地进度。写入时分别落下版本、发现 ID、区域、展厅和更新时间,最后再执行一次刷新;读取时将字符串恢复为领域对象,再交给状态层校验。
async function save(progress: ShanhaiLocalProgress): Promise<void> {
await preferences.put("schema_version", progress.schemaVersion)
await preferences.put("discovered_ids", JSON.stringify(progress.discoveredIds))
await preferences.put("selected_region_id", progress.selectedRegionId)
await preferences.put("selected_hall_id", progress.selectedHallId)
await preferences.put("updated_at", progress.updatedAt)
await preferences.flush()
}
这里避免把每一张卡片的显示状态逐项写入。卡片是否已发现、某区域收录了几只神兽,都应由统一目录和 discoveredIds 推导;当目录内容更新时,旧数据也不会把过期的百分比继续带回界面。存储结构保持很小,也让清除进度、版本迁移和异常恢复都能围绕同一个领域对象进行,而不用遍历每个页面组件。
三、恢复时先校验,再计算
恢复过程先处理版本和未知 ID,再修正已经不在目录中的区域、展厅选择,最后生成首页、区域页和护照页需要的状态。任何一个历史 ID 找不到对应神兽时都会被过滤,避免内容更新后出现无法打开的详情入口。
| 恢复阶段 | 输入 | 输出 |
|---|---|---|
| 数据清洗 | 存储中的版本与发现 ID | 可识别、去重后的发现集合 |
| 位置修正 | 区域与展厅选择 | 仍存在的默认探索位置 |
| 进度派生 | 目录和发现集合 | 区域、展厅进度与护照印章 |
const discoveredCount = region.beastIds
.filter((id: string) => discoveredIds.includes(id)).length
const progress = region.beastIds.length === 0
? 0
: Math.round(discoveredCount * 100 / region.beastIds.length)
护照印章使用同一套集合判断:一个区域内的神兽全部已在发现集合中,才派生出该区域的印章。百分比和印章没有独立写入点,因此不会出现一边已完成、另一边仍停在旧值的分叉。
四、发现动作必须幂等
详情页可能被重复进入,发现动作不能因为重复点击就多次追加同一个 ID。写入前先判断集合中是否存在该神兽;已经存在时保留原集合,只记住用户当前浏览的区域与展厅。首次发现才扩展集合并立即保存。
const nextIds = progress.discoveredIds.includes(beastId)
? progress.discoveredIds
: [...progress.discoveredIds, beastId]
await repository.save({
...progress,
discoveredIds: nextIds,
selectedRegionId: regionId,
selectedHallId: hallId,
updatedAt: Date.now()
})
单独切换探索位置时,逻辑只替换区域、展厅和更新时间,原有发现记录保持不变。这使我看到过什么和我上次停在哪里成为两个可以独立变化、但始终一起恢复的状态。
async function load(): Promise<ShanhaiLocalProgress> {
const schemaVersion = Number(await preferences.get("schema_version", 1))
const rawIds = String(await preferences.get("discovered_ids", "[]"))
const selectedRegionId = String(await preferences.get("selected_region_id", ""))
const selectedHallId = String(await preferences.get("selected_hall_id", ""))
const updatedAt = Number(await preferences.get("updated_at", 0))
let discoveredIds: string[] = []
try {
discoveredIds = JSON.parse(rawIds)
} catch (_) {
discoveredIds = []
}
return {
schemaVersion,
discoveredIds: Array.from(new Set(discoveredIds)),
selectedRegionId,
selectedHallId,
updatedAt
}
}
读取逻辑把可能损坏的 JSON、重复 ID 和已经不存在的目录项看作独立问题。先保证返回对象具有稳定形状,再把目录相关的过滤交给状态层完成。这样本地存储层不需要知道区域页面如何排版,状态层也不需要关心键值读写的细节。
五、重启后的恢复结果
已记录的发现结果和探索位置在强停重启后会从本地存储恢复,并重新生成首页上的发现计数与护照信息。下图展示了恢复后的本地进度界面:它用于说明重启后的记录回读,不把多次详情交互得到的数量变化归因于一次点击。

六、失败不阻断本次探索
本地读取失败时,应用以空进度继续提供离线内容;写入失败时,本次进程中的发现状态仍可继续使用,但下一次启动不会承诺恢复。这个边界让界面有明确的降级语义:浏览和发现不因一次本地写入异常中断,跨重启恢复则以成功写入为前提。对于读者而言,页面不会把一次短暂的存储异常伪装成已经保存成功,也不会因为离线资料仍然可用就阻断接下来的探索。
| 情况 | 当前行为 | 下次启动 |
|---|---|---|
| 读取失败 | 以空进度加载本地内容 | 等待下一次可读的恢复机会 |
| 写入失败 | 保留进程内发现状态 | 不保证已变更内容能够恢复 |
| 历史 ID 失效 | 过滤失效 ID 并重算进度 | 使用清洗后的有效集合 |
七、验收关注点
验收这条链路时,应连续完成一次发现和一次区域或展厅切换,再结束进程并重新进入。恢复后的计数、探索位置和护照信息应来自同一组本地事实;重复发现同一神兽不应增加记录数量;删除或失效的目录项不应把旧选择带回界面。若后续加入账号同步,仍可沿用这一最小事实模型:云端只合并发现集合与位置偏好,区域进度和护照展示继续由本地目录派生,从而避免把不同设备上的展示缓存当成可合并的业务数据。
八、为版本演进留下收口
本地记录需要显式携带版本号,原因并不在于今天的字段已经复杂,而在于图鉴内容、区域划分和默认探索位置都会继续演进。读取旧版本时可以为缺少的字段提供默认值;遇到无法识别的版本时,则返回安全的空进度并保留当前目录作为唯一展示依据。升级逻辑只处理数据形状,不直接修改页面状态,能够减少迁移代码和界面渲染相互牵连的风险。
版本收口还让测试重点更清晰:旧记录能否正常打开、重复 ID 是否收敛、失效选择是否替换为默认位置、升级后的新写入是否带上最新版本。它们分别覆盖历史兼容、数据一致性、导航恢复和后续可维护性。即使存储文件被用户清除,应用也应把这视为一份新的空进度,而不是把不完整内容解释为已完成的探索记录。
function normalize(progress: ShanhaiLocalProgress): ShanhaiLocalProgress {
const version = progress.schemaVersion || 1
const ids = Array.from(new Set(progress.discoveredIds || []))
return {
schemaVersion: version,
discoveredIds: ids,
selectedRegionId: progress.selectedRegionId || "",
selectedHallId: progress.selectedHallId || "",
updatedAt: progress.updatedAt || 0
}
}
function emptyProgress(): ShanhaiLocalProgress {
return { schemaVersion: 1, discoveredIds: [], selectedRegionId: "", selectedHallId: "", updatedAt: 0 }
}
这套边界也适用于后续增加更多图鉴条目时的持续维护。Preferences 的键值持久化、刷新语义和使用限制可参阅 HarmonyOS Preferences 数据持久化文档。
更多推荐



所有评论(0)