羽球搭子 HarmonyOS 实战(15):比赛 SessionStore 与状态恢复

发布时间:2026/9/10 20:46:50

羽球搭子 HarmonyOS 实战(15):比赛 SessionStore 与状态恢复 一、状态恢复不是把一个对象写进磁盘一场双打活动通常包含对局摘要、参与者、轮次、场地、每场比分、当前场次和尚未提交的建局草稿。用户在计分中途切到消息应用、系统回收进程或者登录另一个账号后再回来页面都应该恢复到正确上下文。只保存一个activeSessionId不够标识可能指向已经删除的场次详情可能尚未迁移当前比赛也可能只是页面瞬态状态。更稳妥的做法是把数据分成三层AppStorage负责当前进程内的响应式读取Preferences 保存跨启动数据SessionStore负责键命名、账号隔离、序列化、归一化与恢复顺序。页面只表达“读取当前场次”不直接决定数据从内存还是磁盘取得。HarmonyOS Preferences 的使用方式可参考用户首选项开发指南。二、先定义哪些状态必须跨启动不是所有字段都应该持久化。正在展开的弹窗、按钮按压态、语音播报中的临时标记离开页面后可以丢弃对局摘要、比赛详情、当前对局和建局草稿则直接决定用户能否继续工作应该跨启动保存。当前比赛标识可以只放内存因为它离开计分页后通常需要重新选择当前对局标识则需要持久化首页和统计页都会依赖它。状态生命周期保存位置恢复失败时的处理对局摘要列表跨启动AppStorage Preferences显示空列表并允许新建单场对局详情跨启动AppStorage Preferences跳过损坏项不阻塞其他场次当前对局标识跨页面、跨启动AppStorage Preferences回退到列表第一项或空状态当前比赛标识页面会话AppStorage重新从对阵列表选择建局草稿跨页面、可跨启动AppStorage Preferences使用默认草稿补齐缺失字段这种划分避免了两个常见问题一是把所有 UI 状态都写入磁盘恢复后出现过期弹窗二是只保存详情而没有摘要索引启动时不知道应该枚举哪些详情键。interface RestoredSessionState { summaries: SessionSummary[] details: SessionDetail[] activeSessionId: string activeMatchId: string draft?: SessionDraft migratedKeys: string[] corruptedKeys: string[] }把恢复结果显式建模后启动流程可以一次性提交到内存层同时把迁移和损坏信息交给诊断模块不需要让页面逐个猜测哪些键已恢复。三、账号作用域必须进入键名当应用支持登录时同一台设备可能先后出现游客数据、昵称时代遗留数据和正式账号数据。如果仍然使用固定键g_sessions账号 A 退出后账号 B 会看到前者的场次。键前缀应由稳定用户 ID 生成未登录时保留全局键昵称哈希只能用于兼容旧版本不能继续作为正式身份。private static scopedKey(baseKey: string): string { const user AuthStore.getUser() if (user ! undefined user.id.length 0) { return u_${user.id}_${baseKey} } return baseKey } private static detailKey(sessionId: string): string { return session_detail_${sessionId} }稳定用户 ID 的作用不仅是隔离还让迁移路径可控登录后先查新作用域键找不到时再尝试未分区旧键和昵称哈希旧键一旦读到旧值立即复制到新键。下一次启动就只走新路径不会永久背负多套查找逻辑。四、恢复顺序决定页面是否看到半成品恢复前先清理进程内旧状态再加载摘要然后按摘要逐个加载详情最后恢复当前对局和草稿。若先写入当前对局标识页面监听可能立即触发但对应详情还没有进入AppStorage短暂呈现“场次不存在”。批量恢复完成后再让页面读取可以避免这种状态闪烁。static hydrate(): void { this.clearMemory() const prefs this.prefs if (prefs undefined) return const summaries this.readSummariesWithMigration(prefs) AppStorage.setOrCreateSessionSummary[](sessions, summaries) summaries.forEach((summary: SessionSummary) { const detail this.readDetailWithMigration(prefs, summary.id) if (detail ! undefined) { AppStorage.setOrCreateSessionDetail(this.detailKey(summary.id), this.normalizeDetail(detail)) } }) this.restoreActiveSession(prefs, summaries) this.restoreDraft(prefs) }解析失败不应该导致整个恢复过程终止。某一个详情损坏时可以跳过该项同时保留其他健康场次摘要本身损坏时则回到空列表。恢复函数需要把“没有数据”和“数据损坏”区分开前者是正常首次启动后者应记录诊断信息并提供清理入口。五、写入必须同步更新内存与持久化页面修改比分后如果只更新AppStorage当前画面正确但重启丢失如果只写 Preferences当前页面不会收到响应式更新。统一写入口应先生成不可变的新对象再同时更新两层并在写详情后刷新摘要中的完成场次数和更新时间。static saveDetail(detail: SessionDetail): void { const next this.normalizeDetail(detail) AppStorage.setOrCreateSessionDetail(this.detailKey(next.id), next) this.persist(this.detailKey(next.id), JSON.stringify(next)) } private static persist(key: string, value: string): void { if (this.prefs undefined) return this.prefs.putSync(this.scopedKey(key), value) this.prefs.flush() }不可变更新很重要。对数组原地push后继续把同一引用写回某些 UI 观察链可能无法识别变化复制数组并替换目标元素既便于 ArkUI 刷新也让“保存前值/保存后值”在测试中可比较。六、旧数据迁移要幂等迁移逻辑可能在每次启动执行因此必须满足重复运行不改变结果。优先级可以固定为“新账号键 旧全局键 旧昵称键”。只有新键为空时才复制旧值绝不能用旧值覆盖已经产生的新数据。详情迁移与摘要迁移要使用同一优先级否则会出现新摘要指向旧详情的混合状态。private static readMigrated(prefs: Preferences, key: string): string { const current prefs.getSync(this.scopedKey(key), ) as string if (current.length 0) return current const legacy prefs.getSync(key, ) as string if (legacy.length 0) return prefs.putSync(this.scopedKey(key), legacy) prefs.flush() return legacy }风险错误实现保护策略新数据被旧数据覆盖每次启动都复制旧键仅新键为空时迁移摘要和详情不一致两者采用不同优先级共用同一迁移函数删除后又恢复只删内存不删 Preferences同时删除当前与遗留作用域键JSON 结构升级直接强制断言旧对象通过 normalize 补默认字段七、归一化是版本兼容的最后防线ArkTS 类型只在编译期提供约束磁盘里的 JSON 可能来自旧版本。读取后需要把缺失数组补成空数组、把不存在的参与者引用按姓名映射、把非法比分收敛到非负整数。归一化函数不应修改传入对象而要生成结构完整的新对象避免旧引用继续在页面间传播。private static normalizeMatch(match: MatchItem): MatchItem { return { id: match.id, roundIndex: Math.max(1, match.roundIndex ?? 1), courtIndex: Math.max(1, match.courtIndex ?? 1), teamA: { players: match.teamA?.players?.slice() ?? [] }, teamB: { players: match.teamB?.players?.slice() ?? [] }, scoreA: Math.max(0, Math.floor(match.scoreA ?? 0)), scoreB: Math.max(0, Math.floor(match.scoreB ?? 0)), finishedAt: Math.max(0, match.finishedAt ?? 0) } }对于无法恢复的标识宁可清空并让用户重新选择也不要让页面继续持有悬空 ID。状态恢复的目标不是尽可能保留每一个字节而是恢复一个满足领域约束、能够继续操作的状态。八、用故障矩阵验收恢复链路验收时应覆盖正常退出之外的路径在计分页加分后直接结束进程登录另一个账号删除当前对局人为留下旧版本数据让某个详情 JSON 损坏。每次重新进入后观察首页摘要、对阵进度、当前对局和草稿是否一致。1. 创建含两场比赛的对局完成第一场并记住摘要进度。 2. 在第二场计分到 8:6 后结束应用进程再启动并进入该对局。 3. 确认摘要仍为 1/2第二场未被误标为完成已保存比分保持一致。 4. 切换账号确认两个账号的列表互不可见切回后原数据恢复。 5. 删除当前对局后重启确认不会因为旧详情键而“复活”。 6. 注入缺少新字段的旧 JSON确认归一化后页面可操作且没有崩溃。九、总结SessionStore 的价值不在于封装几个putSync而在于建立一条明确的数据契约页面读写内存状态存储层负责跨启动持久化账号作用域隔离不同用户迁移和归一化保证旧数据可以安全进入新模型。只要恢复顺序、双层写入和删除语义保持一致进程中断就不会把一场比赛变成不可解释的半成品。
延伸阅读

更多相关文章

2026/9/8 2:30:20

LLM应用上线翻车记:5条badcase三轮回血,附线上真实打法

你辛辛苦苦搞了个LLM应用,上线第一天用户就反馈"答非所问"“编造事实”“格式混乱”。你打开日志一看,满屏的翻车记录,但根本不知道从哪开始修。 今天这篇文章,我从一个真实的场景切入:5条badcase如何通过三…

2026/9/5 15:44:14

VC++开发VBScript IDE:原生Windows脚本编辑与调试实战

1. 项目概述:为什么我们需要一个VC开发的VB Script编辑器?如果你是一个长期在Windows平台上进行自动化运维、系统管理或者Office二次开发的工程师,那么对VB Script(VBS)一定不会陌生。从批量处理文件、操作注册表&…

2026/9/10 20:44:19

CANN/GE模型描述获取API

aclmdlGetDescFromFile 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、Ten…

2026/9/10 20:44:19

cann/ge 获取数据集缓冲区API

aclmdlGetDatasetBuffer 【免费下载链接】ge GE(Graph Engine)是面向昇腾的图编译器和执行器,提供了计算图优化、多流并行、内存复用和模型下沉等技术手段,加速模型执行效率,减少模型内存占用。 GE 提供对 PyTorch、Te…

2026/9/10 20:39:18

Python实现轻量级日志监控与警报系统

1. 项目概述:日志监控与警报系统的核心价值日志监控是系统运维中最基础也最重要的环节之一。记得去年我们团队就遇到过一次线上事故——某核心服务突然崩溃,但由于缺乏实时日志监控,直到用户投诉才发现问题,整整耽误了40分钟。这件…

2026/9/10 16:39:38

超人会飞不算本事:系统稳定依赖清晰规则与边界设计

开头先不绕弯子。“#斯坦李吐槽dc 所以超人是无缘无故会飞的嘛哈哈哈哈哈哈哈锤哥真是技术人才啊!#雷神 #复联”这类调侃式短标题,第一波冲击力在于它把两个宇宙的角色塞进同一个吐槽箱里,但细想一下就能发现,它真正碰到的根本不是…

2026/9/10 11:16:38

超人VS蜘蛛侠:拆解超级IP的影响力与传播方法论

把“蜘蛛侠 vs 超人”放在 CSDN 上聊,可能很多人第一反应是走错片场了。但如果把这两个角色看成“两个持续运营了 80 多年的文化产品”,你会发现,这场比较本质上是两个不同 IP 策略的长期结果对比:超人赢在定义了整个超级英雄题材…

2026/9/9 16:31:09

基于CNN的调制信号识别:MATLAB实现时频图分类实战

简介:本资源是一套面向通信工程与信号处理方向学习者、研究者的深度学习实践方案,聚焦调制信号自动检测与识别这一典型无线通信任务,解决传统方法依赖人工特征、低信噪比下性能下降等痛点。压缩包共12个文件(10.73MB)&…

2026/9/10 0:00:55

目录对比去重实战:用哈希算法精准清理重复文件

我电脑里现在还有一块换了三次机的“数据墓地”硬盘,里面存着2016年以前所有旧笔记本的完整备份。平时不觉得有什么,直到前阵子想把它整理归档,发现同一个安装包、同一批照片、同一份论文草稿,在几个不同的备份目录里反复出现。更…

2026/9/10 0:00:55

Leaflet离线地图完整Demo合集:内网部署与坐标纠偏实战

简介:这是一份面向Web GIS开发者的LeafLet离线地图示例合集,帮助开发者快速掌握离线地图从搭建到交互的完整流程。压缩包共723个文件,大小14.06MB,以319个js脚本、175个html页面和29个css样式文件为主体,配合png/svg图…

2026/9/10 0:00:55

MATLAB读取Rinex 3.02观测文件:多系统GNSS数据解析实战

简介:基于MATLAB开发的Rinex3.02版观测文件(o文件)读取代码包,面向卫星定位导航方向的学习者与研究人员,用于解决新版观测文件的数据解析、历元提取与时间转换问题。压缩包共4个文件,包含两个m脚本、一个19…

2026/9/10 12:32:02

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/10 15:19:50

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/10 15:49:53

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
咨询二维码