一次很有代表性的故障发生在校园活动应用从旧版提醒设置升级到新版通知分组:旧版用单个布尔值记录提醒开关,新版拆成报名消息和活动公告,直接读新键会误把所有用户设为默认关闭。乍看只是某个控件或接口的行为不理想,向下追踪却会发现根本原因是数据归属和异常路径没有写成明确约定。

技术主题封面

从一次失败现象开始

小型设置最容易被忽略的是演进。早期一个 notifyEnabled 能满足需求,新版要区分报名消息和活动公告,如果直接读取新键并默认 false,老用户升级后会静默丢失提醒。设置项迁移应像数据结构变化一样被设计。

“数据迁移不是简单复制键;版本必须随迁移成功一起提交,并且缺失键和显式 false 不能混为一谈”是这篇最重要的设计判断。单看成功路径,校园活动应用从旧版提醒设置升级到新版通知分组可以靠若干事件回调工作;但只要用户中途修改、取消或重复操作,若没有清晰约束,就无法确定最终输出究竟属于哪一次输入。工程拆分必须以这些不确定性为起点。

我们希望得到的不是一张孤立的效果图,而是一条可追溯的处理链。整个过程拆为读取版本、映射旧键、写入新键、持久化 flush、更新哨兵。每一段只承诺自己负责的结果,下一段不靠猜测前一步的内部状态。对偏好迁移而言,这种清晰分工也是回归测试能够写得具体的前提。

版本哨兵必须在迁移步骤真正完成后更新。先写版本号再写新键,一旦中间失败,下次启动看到高版本便可能跳过未完成操作。示例采用两阶段 flush 以展示顺序;复杂迁移还应设计恢复与幂等操作。

分层识别根因

实施选择是:读取版本哨兵,按版本执行一次有序迁移,写入后 flush,异常时保留旧键并允许安全重试。这里不能倒过来,先把组件和请求写完,再向现有代码里硬塞判断。规则入口只接收必要数据,输出也尽量可序列化。这样在没有设备的环境下仍能对核心逻辑做确定性检查。

“读取版本”阶段很容易被认为已经完成了整个功能,其实它只提供一个前提。只有进入“映射旧键”之后,才能知道前一步是否真的满足业务要求。把这个检查写成显式返回值,比依赖某个内部字段在恰当时刻改变更稳健。

当路径执行到“映射旧键”,先区分用户仍在编辑、数据已经校验和动作已经确认三种事实。随后执行“写入新键”时,使用同一份中间结果才能避免旧数据回流。日志或测试也应记录阶段,而不是只看最终页面有没有消失。

把“写入新键”到“持久化 flush”视作可中断的边界。任何阶段都可能遇到无效输入或设备状态变化,恢复时应回到上一个仍然可信的事实,而不是为了显示流畅强行跳到最终步骤。这样的路径更适合实际调试。

旧值为 false 不是“没有旧值”。使用适当默认值读取并明确区分数据不存在与用户选择关闭,才能保持原有偏好;如果迁移涉及多种用户意图,应写出映射表而不是只进行布尔复制。

偏好迁移的关键设计

实现一条可控路径

下面给出可以独立检查的关键实现。代码只覆盖本篇要证明的核心行为;涉及工程权限、服务端协议或构建设置的条件已在上下文列明。它应被放入明确的模块中,而不是把所有业务逻辑堆进 ArkUI build()。

import { preferences } from '@kit.ArkData'
import { common } from '@kit.AbilityKit'
async function migrate(context: common.Context): Promise<void> {
  const pref = await preferences.getPreferences(context, 'event_settings')
  const version = Number(await pref.get('schemaVersion', 1))
  if (version >= 2) return
  const legacy = Boolean(await pref.get('notifyEnabled', true))
  await pref.put('notifySignup', legacy)
  await pref.put('notifyBulletin', legacy)
  await pref.flush()  // 先保存新键
  await pref.put('schemaVersion', 2)
  await pref.flush()  // 最后提交版本哨兵
}

Preferences 的优点是轻量、易读写,而不是无限容量或事务数据库。它适合保存少量偏好,不适合保存大文件、离线任务历史或敏感凭证;模块使用场景应与官方约束一致。

代码中的关键判断来自业务约束,而不是某个屏幕比例。以“写入新键”为界,输入应先满足所属阶段的前置条件;当判断失败时,返回错误或保留状态比直接抛出无法解释的页面状态更可控。对需要 UI 反馈的部分,随后由组件转换为明确的文案。

如果把上述核心逻辑接到一个 ArkUI 页面,推荐组件仅处理状态投影和交互事件:例如 TextInput.onChange 接收新的字符串,Button.onClick 触发一次显式动作,得到结果后再设置 @State 驱动展示。对于不是输入框的偏好迁移功能,则采用相同的单向数据边界而不强行引入 TextInput。

偏好迁移的验证示例

重新构造故障

最应该提前考虑的风险是:Preferences 面向少量轻量设置,不适合充当大型任务队列或机密凭证仓库。这句话不是发布时随手附加的注意事项,而是编码时必须坚持的限制。只要异常路径缺乏明确处理,下一次回归就可能把一个本来可恢复的状态变成难以判断的失败。

验证从三种初始目录开始:没有旧配置、旧键 true、旧键 false。升级后读新键,重启再读一次,检查版本号与值是否一致;然后人为构造第一次 flush 失败,验证程序是否可以安全重试。

状态验收。 在校园活动应用从旧版提醒设置升级到新版通知分组中,专门制造一个尚未完成的状态:旧版用单个布尔值记录提醒开关,新版拆成报名消息和活动公告,直接读新键会误把所有用户设为默认关闭。观察模块是否明确阻止下一动作、是否保留用户已经输入的有效内容,以及重新操作后是否会得到可解释的新结果。这个检查的价值在于不把一次成功操作当作稳定性的全部证据。

逆序验收。 把正常操作反过来执行,例如先触发持久化 flush,再补齐写入新键所需要的条件。若界面没有把错误留在错误位置,或者隐式使用上一轮结果,就说明阶段约束并未完全落实。真正的测试应覆盖操作顺序变化,而非只按产品示例步骤点击。

重复验收。 连续两次执行持久化 flush,再执行更新哨兵,检查第二次动作是新意图还是前次动作的继续。对于偏好迁移尤其要明确重复处理规则;“按钮点了两次”是用户行为事实,并不自动意味着应当产生两份业务副作用。

错误恢复。 在读取版本成功、读取版本尚未完成的位置构造异常。应能说明数据暂存在哪里,用户看到何种反馈,是否允许重新触发,以及什么条件下必须停止。不要为了追求一张好看的页面而隐藏正在等待确认的真实状态。

留下一个可维护的边界

配置迁移不是用户能直观看到的新增功能,但它决定升级是否可靠。对外界面只需在必要时展示设置仍在迁移和保存失败,不要把内部 schemaVersion 误解释为应用系统版本。

本篇可用以下结果作为验收目标:从空安装、旧版关闭提醒、旧版开启提醒三种状态升级,结果必须可解释。检验时先从固定输入入手,再加入中途切换、重复操作和失败恢复。可解释的错误结果也属于正确实现的一部分;没有必要为了让每个步骤都显示绿色而悄悄跳过不满足的前置条件。

还有一个容易被忽视的协作问题:页面状态、领域规则和最终输出往往由不同代码文件甚至不同开发者维护。要让改动可控,必须为偏好迁移明确最小接口,规定哪些数据可以跨模块传递、哪些只是展示层的临时变量;代码评审只要看到两处重复决定同一事实,就需要回到单一来源原则。

调试不应从增加大量打印开始。先把读取版本 → 映射旧键 → 写入新键 → 持久化 flush → 更新哨兵按顺序列出来,每次只在边界处记录不含个人信息的阶段与结果类别。这样即使没有真机性能指标,仍能通过构造输入与检查返回值定位哪一步发生分歧;反过来,如果只看最终页面颜色,就难以证明中间没有错误。

关于Preferences 面向少量轻量设置,不适合充当大型任务队列或机密凭证仓库这一风险,建议把它写进实际测试用例的名称和断言,而不是只放在代码注释。注释会随着重构失效,测试中的输入与预期结果更容易暴露行为变化。测试本身同样要避免依赖随时变化的网络与时间,否则会把产品故障和测试环境偶然因素混在一起。

当方案需要扩展,优先问“新需求将改变哪个阶段”。如果它只改变持久化 flush,便不应触及后面的更新哨兵;如果它改变最终业务语义,规则层和序列化层才需要一起调整。这个决策方式能够避免每加一个按钮就复制一份流程逻辑,也使连载中的代码可以逐篇成长。

这类问题的适用边界同样清楚:数据迁移不是简单复制键;版本必须随迁移成功一起提交,并且缺失键和显式 false 不能混为一谈。当具体 SDK、设备能力或服务端协议发生变化,应重新查看对应的官方接口约束,再重新跑上述场景验证,而不是根据一张示例图推断系统行为。

官方资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐