news 2026/7/23 2:01:11

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
羽球搭子 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.setOrCreate<SessionSummary[]>('sessions', summaries) summaries.forEach((summary: SessionSummary) => { const detail = this.readDetailWithMigration(prefs, summary.id) if (detail !== undefined) { AppStorage.setOrCreate<SessionDetail>(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.setOrCreate<SessionDetail>(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,而在于建立一条明确的数据契约:页面读写内存状态,存储层负责跨启动持久化,账号作用域隔离不同用户,迁移和归一化保证旧数据可以安全进入新模型。只要恢复顺序、双层写入和删除语义保持一致,进程中断就不会把一场比赛变成不可解释的半成品。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/23 2:00:34

深入解析Tiva™ MCU时钟系统:从PLL配置到低功耗管理实战

1. 项目概述&#xff1a;微控制器的心脏——时钟系统在嵌入式开发领域&#xff0c;无论是驱动一个简单的LED闪烁&#xff0c;还是处理复杂的实时通信协议&#xff0c;微控制器&#xff08;MCU&#xff09;的每一次“心跳”都至关重要。这个“心跳”的节拍器&#xff0c;就是时钟…

作者头像 李华
网站建设 2026/7/23 1:58:13

Datawhale 量化入门学习笔记:理解波动率

本文是 Datawhale《和Yibo零基础学习量化金融》Phase 2 第五章的学习总结&#xff0c;包含核心概念、理论发展脉络、Python 代码实现和真实行情验证。 标签&#xff1a;量化交易 Python 波动率 学习笔记 Datawhale 投资组合 分类&#xff1a;Python / 量化投资 前言 很多新手只…

作者头像 李华
网站建设 2026/7/23 1:56:01

SaaS云呼叫中心架构实战:从CTI底层原理到企业落地避坑指南

标签&#xff1a;#云呼叫中心 #CTI #SaaS通信 #企业呼叫中心 #外呼风控 #语音中台阅读对象&#xff1a;后端开发、通信架构师、企业IT运维、售前交付、系统集成工程师核心摘要&#xff1a;传统自建呼叫中心硬件成本高、扩容难、运维复杂。本文从CTI通信原理、云呼叫中心分层架构…

作者头像 李华
网站建设 2026/7/23 1:52:38

AMD迷你主机部署vLLM大模型实战指南

1. AMD AI MAX 395迷你主机架构解析这款搭载AMD处理器的迷你主机采用了独特的架构1151设计&#xff0c;在紧凑体积下实现了高性能计算能力。从实际拆机来看&#xff0c;其内部布局经过精心优化&#xff0c;CPU散热模组采用下压式设计配合涡轮风扇&#xff0c;在有限空间内保证了…

作者头像 李华
网站建设 2026/7/23 1:52:36

环境智能系统的核心技术解析与实践

1. 项目概述&#xff1a;当机器开始理解环境2016年AlphaGo击败李世石的那个夜晚&#xff0c;我在机房盯着波动曲线直到凌晨三点。那一刻我突然意识到&#xff0c;我们正在见证的不仅是算法的胜利&#xff0c;更是机器理解复杂环境能力的质变飞跃。如今七年过去&#xff0c;智能…

作者头像 李华