表单编辑中最容易被忽视的时刻,是用户离开当前页面的那几秒。输入框里已经写好内容,页面跳转后组件被重新创建,如果草稿状态只存放在页面变量中,返回时就可能看到旧字段值。对现场巡检而言,反复录入同一段设备描述不仅浪费时间,还可能让后续记录出现不同版本的事实。页面切换需要一套明确的保存承诺,而不是依赖某个组件是否仍然留在内存中。

FormContinuity继续沿用上一章的site-check巡检草稿:schema=1,字段键固定为site、issue、priority。现场地点为“北区巡检点”,摘要为“设备标签磨损,需补贴”,优先级起初为“中”。上一章结束时,摘要编辑产生revision=1,savedRevision=0,草稿处于DIRTY。本章在页面离开前把优先级调整为“高”,形成revision=2,随后完成本地保存与恢复。

HarmonyOS 表单草稿页面切换与保存路线

一、保存的是草稿快照,不是页面对象

一个可恢复的存储记录需要包含草稿标识、数据结构版本、修订号、全部字段和写入时间。页面控件对象、输入框焦点、滚动偏移、窗口宽度都属于临时界面状态,不应该和业务字段一起序列化。若把这些对象直接写入JSON,既可能遇到无法转换的引用,也可能在页面结构调整后产生额外兼容负担。

本章采用一个明确的存储键draft:site-check。整个草稿作为一份JSON字符串保存,避免三个字段分别写入时出现跨字段版本不一致。恢复时先解析并检查schema与draftId,确认字段键齐全、修订号为非负整数,再生成页面可以使用的快照。读到旧结构或缺失字段时进入NEEDS_REVIEW,由迁移逻辑决定如何处理,而不是把不完整数据直接显示给用户。

存储记录中的savedRevision与revision相同,表示该版本已完成持久化;页面内未保存的状态仍由独立的当前修订号表示。如此设计后,用户在草稿列表中看到的“已保存”对应确切版本,而不是上一次按下按钮的时间。读取到损坏数据时还应保留原始错误信息用于诊断,同时给页面一个明确的恢复失败状态。

const FIELD_KEYS = ['site', 'issue', 'priority'];
function encodeDraft(draft) {
  const clean = {};
  for (const key of FIELD_KEYS) {
    if (typeof draft.fields[key] !== 'string')
      throw new Error('INVALID_FIELD_' + key);
    clean[key] = draft.fields[key];
  }
  if (!Number.isSafeInteger(draft.revision) || draft.revision < 0)
    throw new Error('INVALID_REVISION');
  return JSON.stringify({
    draftId: 'site-check', schema: 1, revision: draft.revision,
    savedRevision: draft.revision, fields: clean
  });
}
function decodeDraft(json) {
  const value = JSON.parse(json);
  if (value.draftId !== 'site-check' || value.schema !== 1)
    throw new Error('SCHEMA_REVIEW');
  for (const key of FIELD_KEYS)
    if (typeof value.fields?.[key] !== 'string') throw new Error('FIELD_MISSING');
  if (!Number.isSafeInteger(value.revision) || value.revision < 0)
    throw new Error('REVISION_INVALID');
  return { ...value, status: 'SAVED', changedKeys: [] };
}

二、用首选项存储承接轻量草稿

对于只包含少量短文本和枚举值的草稿,HarmonyOS首选项存储是一个可考虑的持久化选项。华为的@ohos.data.preferences参考文档提供getPreferencesSync(context, options)、putSync(key, value)和flush()等接口,并说明首选项是键值数据存储,适合配置及轻量信息。接口虽然包含同步读写方法,仍需避免在高频输入回调中反复写入磁盘。

为了把接入点固定在存储层,页面只调用saveDraft()或loadDraft(),不直接持有首选项实例。以下ArkTS代码把序列化好的字符串与存储键关联。putSync先更新当前实例,flush用于持久化;保存确认必须以异步操作完成为准。context由页面或Ability的实际上下文传入,具体生命周期管理由调用方负责。

import { preferences } from '@kit.ArkData';

export class DraftRepository {
  private store: preferences.Preferences;

  constructor(context: Context) {
    this.store = preferences.getPreferencesSync(context, {
      name: 'formcontinuity_drafts'
    });
  }

  async saveDraft(payload: string): Promise<void> {
    this.store.putSync('draft:site-check', payload);
    await this.store.flush();
  }

  loadDraft(): string {
    return this.store.getSync('draft:site-check', '') as string;
  }
}

从页面角度看,“保存中”是一种独立状态。触发保存时先冻结当前快照的修订号,随后写入完整序列化结果。完成后只有当页面当前修订号仍等于该快照修订号,才能把界面状态改成SAVED。如果写入过程中用户又修改了字段,旧保存即使成功,也只能确认旧版本已写入,不能消除新的未保存修改。

三、连续编辑时要拦截过期保存确认

页面离开前进行保存与自动保存同时发生时,最危险的情况是两个写入任务乱序结束。例如版本1的保存先开始,版本2的保存后开始,但版本1由于排队时间更长,反而在最后完成。若两个任务都允许写入相同的键,较旧内容就可能覆盖较新内容。因此同一草稿必须使用串行写入队列,保证实际存储顺序与提交顺序一致。

保存协调器给每次请求分配递增序号,并把写入任务串接到前一个Promise之后。新的请求可以在旧请求尚未完成时进入队列,但最终写入顺序受队列控制。返回的确认结构包含已保存修订号,页面收到确认后再对比当前版本。这样既可以处理快速跳转,也可以处理用户短时间连续更改内容的情形。

class SaveQueue {
  constructor(repository) {
    this.repository = repository;
    this.tail = Promise.resolve();
    this.sequence = 0;
  }
  save(draft) {
    const sequence = ++this.sequence;
    const payload = encodeDraft(draft);
    const work = this.tail.catch(() => {}).then(async () => {
      await this.repository.put('draft:site-check', payload);
      return { sequence, savedRevision: draft.revision };
    });
    this.tail = work;
    return work;
  }
}

const memoryStore = new Map();
const repo = { put: async (key, value) => memoryStore.set(key, value) };
const queue = new SaveQueue(repo);
const v2 = {
  draftId: 'site-check', schema: 1, revision: 2,
  fields: { site: '北区巡检点', issue: '设备标签磨损,需补贴', priority: '高' }
};
queue.save(v2).then(result => console.log(result.savedRevision));

HarmonyOS 表单草稿保存与读取开发图

四、返回页面先恢复,再连接输入控件

页面重新进入时应先定位draftId,从存储中读取完整JSON,解码后再创建字段组件。不能先把默认值展示给用户、等异步读取结束再无条件覆盖,因为用户可能已经开始新一轮输入。可以先显示“正在恢复”,成功后绑定快照,失败时给出明确的重新录入和查看草稿选项。页面导航不应直接改变草稿的修订号。

对于本章的数据,完整过程为:摘要更改后版本1处于待保存状态;优先级从“中”改为“高”后形成版本2;页面切换触发序列化,保存完成时savedRevision=2;返回编辑页时恢复版本2,changedKeys清空且状态为SAVED。地点“北区巡检点”和摘要“设备标签磨损,需补贴”保持不变。这个顺序也是业务回归检查的基准,不能只检查屏幕上出现了“已保存”三个字。

业务层还需要防止重复创建:恢复过程中如果旧页面实例仍持有一次提交动作,应以草稿标识和保存队列进行去重。对于已经确认写入的版本2,再次打开页面不应产生版本3。只有用户下一次实际编辑字段,才递增revision。这与上一章的“相同值不增加修订号”规则一致,避免因页面生命周期导致版本不断膨胀。

五、错误恢复不应覆盖较新的编辑

保存可能因空间不足、目录权限变化或数据格式异常而失败。页面遇到错误时必须保留内存中的最新快照,将状态改为SAVE_ERROR,并允许用户重新尝试。失败日志可以记录错误码、草稿标识与修订号,但不应该完整打印用户填写的地点和故障内容。这样既便于定位问题,也避免在技术诊断记录中扩散表单数据。

恢复也有两类明显区别:查不到任何键时可以进入新表单初始化流程;查到键但无法解析时应标记数据异常,避免直接覆盖。对于结构版本不匹配的记录,最好先进入待迁移分支。下一章节会讨论不同来源的字段修订冲突,因此当前保存格式保留了稳定主键、修订号和结构版本,为后续比较提供依据。

首选项接口的缓存和多进程使用条件应在工程设计中考虑。官方参考文档指出首选项不适合多进程同时操作同一实例;本项目在一个应用进程内串行提交草稿,若日后涉及跨设备数据同步,应引入独立的同步协议与冲突解决策略,而不是把本地首选项当作共享数据库。

六、用明确状态检查页面切换的完整路径

可以检查四个时间点:离开前当前修订号2且状态为DIRTY;保存队列接收版本2;存储确认后状态变为SAVED;返回页面时三个字段与版本2完全一致。若用户在保存请求发出后又修改摘要,页面应保留更高修订号和DIRTY,避免旧请求的成功回执覆盖新编辑。这些检查都是业务状态判定,不依赖特定窗口尺寸或导航动画。

在页面中,存储确认可以展示为“已保存 · revision 2”,而不是以某个固定时间代替版本。现场地点仍是“北区巡检点”,摘要仍是“设备标签磨损,需补贴”,优先级现为“高”,draftId=site-check与schema=1不变。此时“保存”的语义已经从按钮动作变成可重复读取的数据事实,后续修改布局或增加冲突处理时也更容易追踪。

HarmonyOS 表单恢复后的本地状态

七、后续扩展仍沿用同一套草稿身份

本章增加的存储层只处理完整快照,不解释字段间的业务冲突。下一章面对不同来源的更新时,会继续使用site、issue、priority作为比较单元,并把版本2作为本地参考快照。为了减少迁移成本,页面不应把字段显示顺序写进业务主键,存储也不需要知道每个字段位于左列还是右列。

至此,FormContinuity从固定字段定义进入了可恢复草稿阶段:编辑动作负责有效修订,序列化负责结构约束,存储队列负责写入顺序,页面负责根据确认版本展示状态。把各自职责分开,才能在后续增加冲突比较和多尺寸布局时继续保持数据来源清晰、交互规则稳定。

参考文档:华为开发者联盟·User Preferences API、华为开发者联盟·TextInput输入与自定义键盘、华为开发者联盟·@Watch状态监听。

Logo

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

更多推荐