ArkData Preferences:折叠屏草稿防抖写入与重入恢复【鸿蒙心迹】
一个多形态应用里,用户刚把一段文字敲完,就合上折叠屏,再展开继续编辑。页面看起来只是换了个尺寸,工程上却有两个完全不同的问题:组件可能只是重新布局,也可能因导航、进程回收或页面重建而失去内存状态。只把输入内容写在 @State 里,解决的是当前组件的界面刷新,不等于草稿已经落盘。
这次用一个范围很窄的 FoldDraft 演示:草稿编号 D-214,标题“湖边步道拍摄清单”,正文示例“傍晚先拍桥,再拍水面倒影。”。关注的不是折叠屏铰链角度,而是无论窗口怎样变化,输入能否以可解释的方式保存和恢复。示意图中的“已保存 14:38:20”只是预设演示状态,不是设备实测时间。

一、不要把界面状态当成持久化承诺
TextArea 绑定 @State 后,输入时会更新当前页面。折叠展开时,组件有机会只响应尺寸变化;这时同一份状态仍在,往往看不出问题。但如果用户在编辑后切到其他页面,或者系统后来销毁了原有实例,再创建出来的 @State 就不是原来的那份。我们不能根据“折叠一下文字没丢”推断数据安全。
我把草稿状态拆成三层。第一层是输入缓冲区,负责即时显示;第二层是待写入状态,告诉用户内容还没被确认持久化;第三层才是 Preferences.flush() 完成后的状态。UI 展示“已保存”,必须由第三层驱动,不能在 onChange 一开始就修改。这里的 500 毫秒,是演示项目主动选择的防抖参数,不属于 ArkData 的系统固定值。
这一边界容易被忽略:Preferences 是应用内轻量键值数据能力,不是跨设备同步服务,也不适合存无限增长的大文档。它同样不能保证进程被强杀前最后半秒的输入必然落盘。真正需要高价值内容零丢失承诺时,仍要设计更严格的保存流程、数据模型与异常恢复策略。
二、把单条草稿的读写收进一个对象
页面直接到处调用数据接口,后面很难知道哪些保存还在排队。为了解决这个问题,先做一个 DraftStore.ets。使用官方 @kit.ArkData 中的 preferences,以 FoldDraftPrefs 作为本应用示例存储名,用 draft:D-214 避免不同草稿的键冲突。
// entry/src/main/ets/model/DraftStore.ets
import { preferences } from '@kit.ArkData';
import { common } from '@kit.AbilityKit';
export class DraftStore {
private prefs: preferences.Preferences;
private pending: Promise<void> = Promise.resolve();
constructor(context: common.UIAbilityContext) {
this.prefs = preferences.getPreferencesSync(context,
{ name: 'FoldDraftPrefs' });
}
read(id: string): string {
return this.prefs.getSync(`draft:${id}`, '') as string;
}
write(id: string, value: string): Promise<void> {
const next = this.pending.catch(() => {}).then(async () => {
this.prefs.putSync(`draft:${id}`, value);
await this.prefs.flush();
});
this.pending = next;
return next;
}
}
这里的序列 Promise 主要解决写入顺序:用户连续更改文本时,后一次请求不会越过前一次完成。putSync 更新的是当前 Preferences 对象里的键值,后面显式调用 flush,并且等待其 Promise 返回后再通知 UI。官方文档说明,XML 存储使用 flush 持久化,GSKV 存储的行为不同;示例使用普通配置,不擅自假定任何设备都启用了某种存储类型。
还有一个异常细节:pending.catch(() => {}) 允许前一次失败后队列继续前进,但并没有偷偷把失败标记为成功。本次调用返回的 next 仍会拒绝,页面需要自己提示。正式项目还可以加失败重试和写入日志,但不要在异常中无限循环刷盘。
三、输入事件是保存起点,不是保存完成点
页面的职责变得清楚:进入时读取草稿;用户输入时立即更新 @State;安静 500 毫秒再落盘;点击“保存草稿”时直接触发一次保存。下面摘出核心状态方法,getContext(this) 取得当前 Stage 页面上下文,页面文件需导入 common 以及前面的 DraftStore。
// DraftEditorPage.ets 中的核心状态与方法
private readonly draftId: string = 'D-214';
private store?: DraftStore;
private timer: number = -1;
private revision: number = 0;
@State draftText: string = '傍晚先拍桥,再拍水面倒影。';
@State saveLabel: string = '待恢复';
aboutToAppear(): void {
try {
this.store = new DraftStore(getContext(this) as common.UIAbilityContext);
const saved = this.store.read(this.draftId);
if (saved.length > 0) this.draftText = saved;
this.saveLabel = '本地草稿已加载';
} catch (e) { this.saveLabel = '恢复失败,请勿覆盖原稿'; }
}
onTextChanged(value: string): void {
this.draftText = value;
const rev = ++this.revision;
this.saveLabel = '待保存';
if (this.timer >= 0) clearTimeout(this.timer);
this.timer = setTimeout(() => { this.saveRevision(value, rev); }, 500);
}
async saveRevision(value: string, rev: number): Promise<void> {
if (!this.store) return;
try {
await this.store.write(this.draftId, value);
if (rev === this.revision) this.saveLabel = '已保存';
} catch (e) { this.saveLabel = '保存失败,请重试'; }
}
revision 是本篇真正要保留的变量。假设用户输入 A,保存开始;随后又改成 B。即便 A 的 flush 先返回,它也不能把 B 对应的“待保存”覆盖成“已保存”。比较修订号后,只有最新内容保存完成才会更新提示。这个修订号只用于同一页面实例内部,不是跨设备冲突版本。
还要注意“空字符串”与“没有存储记录”并非一回事。上面为了让片段短一些,用空串作为恢复默认值;如果产品允许用户彻底清空并保存草稿,就应该另设是否存在键或时间戳,避免把一次合法清空误判为首次进入。

这张 DevEco 白色主题图是开发界面演示示意:左侧展示 DraftEditorPage.ets、DraftStore.ets,中间关注恢复与防抖流程,右侧显示 D-214,底部日志演示写入、完成及重新加载的事件顺序。它不是这段代码在真机上的已验证运行结果;具体导包及实现请以正文和当前官方 API 参考为准。
四、界面提示应反映写入链路的真实阶段
状态文案我只保留四种:本地草稿已加载、待保存、已保存、保存失败,请重试。如果再给它配一个毫无依据的“同步成功”,用户会误以为数据已经去了云端。尤其在折叠屏这样的连续交互中,界面可能每次都很丝滑,但这和持久化是否成功不是同一个维度。
第三段代码解决“显示什么”和“点击保存怎样处理”两个问题。TextArea 对文字改动触发回调,按钮触发显式保存。退出组件时清掉定时器,防止已经销毁的实例继续调度 UI 更新。下面与第二段代码属于同一个页面组件的不同摘录,不是另一份同名页面。
saveImmediately(): void {
if (this.timer >= 0) clearTimeout(this.timer);
this.timer = -1;
this.saveRevision(this.draftText, this.revision);
}
aboutToDisappear(): void {
if (this.timer >= 0) clearTimeout(this.timer);
this.timer = -1;
}
build() {
Column({ space: 16 }) {
Text('湖边步道拍摄清单').fontSize(22)
Text(`草稿 ID:${this.draftId}`)
TextArea({ text: this.draftText, placeholder: '记录拍摄想法' })
.height(180)
.onChange((value: string) => this.onTextChanged(value))
Text(this.saveLabel)
Button('保存草稿').onClick(() => this.saveImmediately())
}.padding(20)
}
上面的 aboutToDisappear 没有把“最后一次异步写盘”当成系统必定执行的保险。组件销毁回调中强行等待 Promise 不合适,且后台、崩溃、断电、进程被杀和折叠状态变化,也不总会按同一个页面生命周期顺序发生。实际产品应在编辑中持续增量保存,在离开页面时提供清晰的待保存提示;必要时用显式保存控制高价值操作。

手机图展示的是预设的“已保存”状态:D-214、内容示例和本地保存时间 14:38:20。图中按钮、保存标识和本地草稿属于同一个 Demo;图片是设计模拟而非真机截图。即便示意里的视觉元素提到其他设备,也不代表本篇完成了跨设备同步——这里展示的所有数据仍局限于本地 Preferences。
五、怎么证明这个方案没有掩盖错误
验收不能只点一次按钮看界面是否好看。建议依次做四组可复现的开发测试:先连续输入三段不同文字,检查“待保存”没有被旧 Promise 回调提前盖掉;再输入后立即切换布局,验证页面在未销毁情况下不会清空内容;接着手动退出并重进页面,检查 draft:D-214 是否被恢复;最后在写入时注入可控制的失败,核对“保存失败”是否可见。
另外一组容易漏掉:在防抖的 500 毫秒之内强制结束应用。这样的情况下,最后一次编辑可能没有进入持久化文件。必须把这写入设计边界,而不是为了让文章结论漂亮就称“彻底解决草稿丢失”。图里的日志与耗时仅用于说明应该记录哪些事件,不构成设备测量数据。
多进程并发写同一个 Preferences 文件也不适合本方案。官方明确指出首选项不保证进程并发安全;未来若出现多模块、大文档、冲突合并需求,应调整数据层而不是继续往一个 KV 文件里塞字段。至此,FoldDraft 的目标可以准确表述为:让常见页面重入有恢复依据,让编辑中的保存状态可解释,同时保留对最后短窗口丢失与异常写入的诚实说明。
六、参考与适用范围
本文依据华为开发者联盟《用户首选项 API 参考》(页面更新时间 2026-09-09)、ArkUI TextArea 文本绑定问题说明及页面生命周期文档组织示例。官方入口:
- https://developer.huawei.com/consumer/cn/doc/harmonyos-references/js-apis-data-preferences
- https://developer.huawei.com/consumer/cn/doc/doccenter-dev-faq/faqs-arkui-1615
- https://developer.huawei.com/consumer/cn/doc/harmonyos-guides-v5/arkts-page-custom-components-lifecycle-V5
代码示例面向 Stage 模型和支持所用 API 的 SDK;截至本文制作时,未在 DevEco Studio 真机编译执行,图文均为可供实现与检查的演示稿,具体项目仍应以目标 SDK 的检查结果为准。
更多推荐




所有评论(0)