鸿蒙 ArkTS 实战:数据存储 Preferences 本地持久化(示例 95)

引言
数据持久化是应用开发中不可回避的核心主题。无论是记住用户的登录状态、保存应用的配置偏好、还是缓存最近浏览的记录,都需要将数据存储到设备的本地存储中,确保应用关闭后数据不会丢失。HarmonyOS NEXT 提供了多种数据存储方案,其中 Preferences 是最轻量级的键值对存储 API,适用于保存少量的简单数据——如用户信息、设置项、标记位等。它的底层实现基于 XML 文件,使用内存缓存 + 异步刷盘的策略,既保证了读取速度,又确保了数据持久性。
示例 95 以「用户信息存储」为业务场景,实现了一个完整的 Preferences 增删改查演示页面。用户可以输入姓名和年龄,点击「保存」将数据持久化到本地;点击「读取」从本地存储中加载数据并显示;点击「清除」删除已保存的数据。页面在 aboutToAppear 生命周期中自动加载已保存的数据,确保用户每次进入页面都能看到上次保存的内容。整个流程涉及 preferences.getPreferencesSync 获取实例、putSync 写入数据、getSync 读取数据、deleteSync 删除数据、flush 刷盘持久化、以及 try-catch 异常处理,几乎涵盖了 Preferences API 的全部核心用法。
这篇文章会严格按源码顺序,先介绍应用的整体功能与布局结构,再拆解 Preferences API 的核心方法,接着逐段解读 .ets 源码中的保存、读取、清除逻辑和生命周期回调,然后分析同步 API 的使用场景、异常处理策略、数据类型转换的注意事项,最后给出运行操作指南、可扩展方向与常见问题调试技巧。读完后,你不仅能看懂这一个数据存储页面,还能举一反三,把它应用到用户设置持久化、表单草稿保存、应用首次启动标记等任何需要本地存储的场景。
1. 应用概述与功能
「数据存储」是一个面向数据持久化场景的工具型页面,交互路径清晰:输入数据 → 保存 → 读取验证 → 可清除。
页面自上而下分为四块区域:顶部返回栏(返回按钮 + 标题「数据存储」);主操作卡片(标题 + 姓名输入框 + 年龄输入框 + 保存/读取/清除三按钮 + 已保存数据展示区);底部提示文字(「应用重启后数据仍然保留,试试重新进入本页」)。
1.1 核心功能清单
- 保存数据:将输入的姓名和年龄通过
Preferences.putSync写入本地存储,并调用flush持久化到磁盘。 - 读取数据:通过
Preferences.getSync从本地存储中读取已保存的姓名和年龄,显示在卡片中。 - 清除数据:通过
Preferences.deleteSync删除已保存的键值对,并清空输入框。 - 自动加载:页面
aboutToAppear时自动调用load()方法,加载已保存的数据。 - 异常处理:所有 Preferences 操作都包裹在
try-catch中,出错时弹出 Toast 提示。 - 操作反馈:每次保存、读取、清除操作后都通过
promptAction.showToast弹出提示。
1.2 技术要点一览
整个示例用到的关键技术对「数据持久化」类页面很有代表性:preferences.getPreferencesSync 获取 Preferences 实例、putSync / getSync / deleteSync 三个同步读写删方法、flush 确保数据刷盘、try-catch 异常捕获与 Toast 反馈、aboutToAppear 生命周期自动加载、as 类型断言处理 getSync 的返回值、以及 InputType.Number 限制年龄输入为数字。把这些要点串起来,就构成了一条完整的「用户输入 → 持久化写入 → 磁盘存储 → 读取回显 → 删除清理」的数据生命周期。
2. 核心知识点
在逐段读代码之前,先把数据存储页面承载的 ArkTS 和 HarmonyOS 核心知识讲清楚。
2.1 Preferences API 概述
Preferences 是 HarmonyOS 提供的轻量级数据存储 API,来自 @kit.ArkData 模块。它的特点是:
- 键值对存储:数据以 key-value 形式存储,key 为字符串,value 支持 number、string、boolean、Array 等类型。
- 内存缓存:数据首先缓存在内存中,读取时直接从内存获取,速度极快。
- 异步刷盘:调用
flush()后,内存中的数据才会写入磁盘文件。如果不调用flush(),应用异常退出时数据可能丢失。 - 同步与异步:API 同时提供同步版本(
putSync、getSync等)和异步版本(put、get等返回 Promise),本示例使用同步版本简化代码。
2.2 getPreferencesSync 获取实例
使用 Preferences 的第一步是获取实例:
import { preferences } from '@kit.ArkData';
const pref: preferences.Preferences = preferences.getPreferencesSync(getContext(this), { name: 'userStore' });
getPreferencesSync 接收两个参数:
- context:应用上下文,通过
getContext(this)获取。Preferences 文件存储在应用沙箱目录下,需要 context 来确定存储路径。 - options:配置对象,
{ name: 'userStore' }指定 Preferences 文件名。不同的 name 对应不同的文件,可以用于区分不同模块的数据。
返回值是 preferences.Preferences 类型实例,后续所有读写操作都通过这个实例进行。
2.3 putSync 写入数据
pref.putSync('name', this.name);
pref.putSync('age', this.age);
pref.flush();
putSync 接收两个参数:key(字符串)和 value(支持多种类型)。数据首先写入内存缓存,然后需要调用 flush() 将内存数据刷入磁盘文件。
flush() 是一个同步方法,调用后数据会被写入磁盘。如果不调用 flush(),数据只在内存中,应用正常退出时可能会自动刷盘,但异常退出(如崩溃、强杀)时数据会丢失。所以最佳实践是每次 putSync 后都调用 flush()。
2.4 getSync 读取数据
const n: string = pref.getSync('name', '') as string;
const a: string = pref.getSync('age', '') as string;
getSync 接收两个参数:key(字符串)和 defaultValue(默认值)。如果 key 不存在(未保存过或已删除),返回 defaultValue。
getSync 的返回值类型是 ValueType,可能是 number、string、boolean 等。由于我们存储的是字符串,需要用 as string 进行类型断言,将返回值转为 string 类型。这是 ArkTS 静态类型系统的要求——如果不做类型断言,编译器无法确定返回值的具体类型。
2.5 deleteSync 删除数据
pref.deleteSync('name');
pref.deleteSync('age');
pref.flush();
deleteSync 接收一个参数:key(字符串)。删除指定 key 的数据。与 putSync 一样,删除操作也先在内存中执行,需要调用 flush() 确保删除操作持久化到磁盘。
2.6 try-catch 异常处理
所有 Preferences 操作都包裹在 try-catch 中:
try {
const pref: preferences.Preferences = preferences.getPreferencesSync(getContext(this), { name: 'userStore' });
pref.putSync('name', this.name);
pref.putSync('age', this.age);
pref.flush();
promptAction.showToast({ message: '已保存' });
this.load();
} catch (err) {
promptAction.showToast({ message: '保存失败' });
}
Preferences 操作可能因多种原因失败:存储空间不足、文件权限问题、数据类型不匹配等。try-catch 捕获这些异常,在 catch 块中通过 Toast 提示用户操作失败,避免应用崩溃。
注意 ArkTS 中 catch 子句不需要(也不允许)声明变量类型标注,直接使用 catch (err) 即可,编译器会自动推断 err 的类型。
2.7 aboutToAppear 生命周期
aboutToAppear(): void {
this.load();
}
aboutToAppear 是 ArkUI 组件的生命周期回调,在组件创建后、build 方法执行前被调用。在这个时机调用 load() 方法,可以在页面渲染前加载已保存的数据,确保用户进入页面时就能看到上次保存的内容。
这个设计非常关键——如果不在 aboutToAppear 中加载数据,用户每次进入页面时「已保存的数据」区域都是空的,只有手动点击「读取」按钮才能看到数据,体验很差。
3. 源码逐段解析
现在开始按源码顺序逐段解读 index95.ets,从导入声明到 build 方法,完整展示数据存储的实现细节。
3.1 导入声明与组件声明
import { router } from '@kit.ArkUI';
import { promptAction } from '@kit.ArkUI';
import { preferences } from '@kit.ArkData';
@Entry
@Component
struct Index95 {
源码导入了三个模块:router(页面导航)、promptAction(轻提示)、preferences(数据存储)。注意 preferences 来自 @kit.ArkData 而非 @kit.ArkUI——它是 ArkData 模块的一部分,专门用于数据存储。
3.2 状态变量定义
@State name: string = '';
@State age: string = '';
@State saved: string = '';
三个 @State 状态变量:
name:姓名输入框的内容,与TextInput双向绑定。age:年龄输入框的内容,与TextInput双向绑定。saved:已保存数据的展示文本,格式为「姓名:XXX 年龄:YYY」或「暂无已保存的数据」。
注意 age 使用 string 类型而非 number,这是因为 TextInput 的 onChange 回调返回的是字符串,直接用字符串存储更方便。需要数值运算时再通过 Number() 转换。
3.3 save 保存方法
private save(): void {
try {
const pref: preferences.Preferences = preferences.getPreferencesSync(getContext(this), { name: 'userStore' });
pref.putSync('name', this.name);
pref.putSync('age', this.age);
pref.flush();
promptAction.showToast({ message: '已保存' });
this.load();
} catch (err) {
promptAction.showToast({ message: '保存失败' });
}
}
save 方法的执行流程:
- 获取实例:
getPreferencesSync获取名为userStore的 Preferences 实例。 - 写入数据:
putSync('name', this.name)和putSync('age', this.age)将姓名和年龄写入内存缓存。 - 刷盘持久化:
flush()将内存数据写入磁盘文件。 - 提示成功:
showToast弹出「已保存」提示。 - 刷新展示:调用
this.load()重新读取数据,更新「已保存的数据」展示区。 - 异常处理:如果任何步骤出错,
catch块弹出「保存失败」提示。
保存后立即调用 load() 是一个好的设计——让用户立即看到保存结果,确认数据已正确存储。
3.4 load 读取方法
private load(): void {
try {
const pref: preferences.Preferences = preferences.getPreferencesSync(getContext(this), { name: 'userStore' });
const n: string = pref.getSync('name', '') as string;
const a: string = pref.getSync('age', '') as string;
this.saved = (n.length > 0 || a.length > 0)
? '姓名:' + n + ' 年龄:' + a
: '暂无已保存的数据';
} catch (err) {
this.saved = '读取失败';
}
}
load 方法的执行流程:
- 获取实例:与
save方法使用相同的name: 'userStore',确保读写同一个存储实例。 - 读取数据:
getSync('name', '')读取姓名,如果 key 不存在返回空串。as string进行类型断言。 - 格式化展示:如果姓名或年龄任一不为空,拼接为「姓名:XXX 年龄:YYY」;都为空则显示「暂无已保存的数据」。
- 异常处理:出错时设置
this.saved = '读取失败'。
注意 getSync 的第二个参数是默认值——当 key 不存在时返回这个值。这里使用空串 '' 作为默认值,便于后续用 length > 0 判断是否有数据。
3.5 clearData 清除方法
private clearData(): void {
try {
const pref: preferences.Preferences = preferences.getPreferencesSync(getContext(this), { name: 'userStore' });
pref.deleteSync('name');
pref.deleteSync('age');
pref.flush();
this.name = '';
this.age = '';
this.load();
promptAction.showToast({ message: '已清除' });
} catch (err) {
promptAction.showToast({ message: '清除失败' });
}
}
clearData 方法的执行流程:
- 获取实例:同样使用
userStore实例。 - 删除数据:
deleteSync('name')和deleteSync('age')分别删除姓名和年龄的键值对。 - 刷盘持久化:
flush()确保删除操作写入磁盘。 - 清空输入框:
this.name = ''和this.age = ''清空输入框内容。 - 刷新展示:调用
this.load()更新展示区,此时会显示「暂无已保存的数据」。 - 提示成功:弹出「已清除」提示。
- 异常处理:出错时弹出「清除失败」。
清除操作不仅删除存储中的数据,还清空了输入框,让页面恢复到初始状态。
3.6 aboutToAppear 生命周期
aboutToAppear(): void {
this.load();
}
在组件创建时自动调用 load() 方法,加载已保存的数据。这确保了用户每次进入页面时,「已保存的数据」区域都能正确显示上次保存的内容——即使应用已经重启过。
3.7 build 方法整体结构
build() {
Column() {
// 顶部返回栏
Row() { ... }
// 主操作卡片
Column({ space: 14 }) {
Text('Preferences 本地键值存储') ...
// 姓名输入
Column({ space: 6 }) { ... }
// 年龄输入
Column({ space: 6 }) { ... }
// 操作按钮行
Row({ space: 12 }) { ... }
// 已保存数据展示
Column({ space: 6 }) { ... }
}
// 底部提示
Text('应用重启后数据仍然保留...') ...
Blank()
}
.width('100%')
.height('100%')
.backgroundColor('#f2f3f5')
}
3.8 顶部返回栏
Row() {
Button('返回')
.backgroundColor('#1a6cff')
.fontColor(Color.White)
.onClick(() => {
router.back();
})
Text('数据存储')
.fontSize(18)
.fontWeight(FontWeight.Bold)
Blank()
}
.width('100%')
.padding({ left: 12, right: 12, top: 10, bottom: 10 })
标准返回栏,蓝色返回按钮 + 标题 + Blank() 弹性占位。
3.9 主操作卡片
主操作卡片是页面的核心区域,使用 Column({ space: 14 }) 布局,内部包含多个子区域:
Column({ space: 14 }) {
Text('Preferences 本地键值存储')
.fontSize(13)
.fontColor('#999999')
// 姓名输入
Column({ space: 6 }) {
Text('姓名').fontSize(13).fontColor('#666666').width('100%')
TextInput({ text: this.name, placeholder: '输入姓名…' })
.width('100%')
.onChange((v: string) => {
this.name = v;
})
}
.width('100%')
// 年龄输入
Column({ space: 6 }) {
Text('年龄').fontSize(13).fontColor('#666666').width('100%')
TextInput({ text: this.age, placeholder: '输入年龄…' })
.width('100%')
.type(InputType.Number)
.onChange((v: string) => {
this.age = v;
})
}
.width('100%')
// 操作按钮行
Row({ space: 12 }) {
Button('保存').layoutWeight(1).height(44).backgroundColor('#1a6cff').fontColor(Color.White)
.onClick(() => { this.save(); })
Button('读取').layoutWeight(1).height(44).backgroundColor('#ff8f1f').fontColor(Color.White)
.onClick(() => { this.load(); })
Button('清除').layoutWeight(1).height(44).backgroundColor('#ff4d4f').fontColor(Color.White)
.onClick(() => { this.clearData(); })
}
.width('100%')
// 已保存数据展示
Column({ space: 6 }) {
Text('已保存的数据:').fontSize(13).fontColor('#999999')
Text(this.saved)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor('#333333')
.width('100%')
.padding(12)
.backgroundColor('#f8f9fa')
.borderRadius(8)
}
.width('100%')
}
.width('92%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(14)
.margin({ top: 16 })
主操作卡片包含以下部分:
- 标题说明:「Preferences 本地键值存储」灰色小字说明功能。
- 姓名输入区:标签 +
TextInput,text: this.name双向绑定。 - 年龄输入区:标签 +
TextInput,type: InputType.Number限制只能输入数字。 - 操作按钮行:三个按钮等宽排列(
layoutWeight(1)),颜色区分功能——蓝色保存、橙色读取、红色清除。 - 已保存数据展示:灰色背景圆角卡片,显示已保存的数据或提示文字。
InputType.Number 是一个重要的细节——它让年龄输入框只接受数字输入,自动弹出数字键盘,避免用户输入非数字字符。
3.10 底部提示
Text('应用重启后数据仍然保留,试试重新进入本页')
.fontSize(12)
.fontColor('#999999')
.margin({ top: 14 })
Blank()
底部提示引导用户验证数据持久化效果。Blank() 在底部占据剩余空间,将提示文字推到合适的位置。
4. 交互流程详解
4.1 首次进入页面
aboutToAppear生命周期触发,调用load()方法。getPreferencesSync获取userStore实例(如果文件不存在,会自动创建)。getSync('name', '')返回默认值空串(因为还没有保存过数据)。getSync('age', '')同样返回空串。this.saved设为「暂无已保存的数据」。- 页面渲染,显示空输入框和「暂无已保存的数据」。
4.2 保存数据
用户输入姓名「张三」和年龄「25」,点击「保存」:
save()方法被调用。putSync('name', '张三')将姓名写入内存缓存。putSync('age', '25')将年龄写入内存缓存。flush()将内存数据写入磁盘文件。- Toast 弹出「已保存」。
this.load()被调用,重新读取数据。this.saved更新为「姓名:张三 年龄:25」。- 展示区刷新,显示新保存的数据。
4.3 读取数据
用户点击「读取」按钮:
load()方法被调用。getSync('name', '')返回「张三」。getSync('age', '')返回「25」。this.saved更新为「姓名:张三 年龄:25」。
读取操作总是反映磁盘上的最新数据,与输入框中的内容无关。
4.4 清除数据
用户点击「清除」按钮:
clearData()方法被调用。deleteSync('name')删除 name 键。deleteSync('age')删除 age 键。flush()将删除操作写入磁盘。this.name = ''和this.age = ''清空输入框。this.load()被调用,getSync返回默认值空串。this.saved更新为「暂无已保存的数据」。- Toast 弹出「已清除」。
4.5 应用重启后验证
用户保存数据后关闭应用,重新打开:
aboutToAppear触发,调用load()。getSync从磁盘文件读取上次保存的数据。- 展示区显示「姓名:张三 年龄:25」。
这就是数据持久化的核心价值——数据在应用关闭后仍然保留。
5. UI 样式设计思路
5.1 三色按钮区分功能
三个操作按钮使用不同颜色:
- 蓝色
#1a6cff(保存):主题色,表示主要操作。 - 橙色
#ff8f1f(读取):暖色,表示查询操作。 - 红色
#ff4d4f(清除):警示色,表示危险操作。
三色按钮让用户一眼就能区分不同功能,降低误操作风险。
5.2 数据展示卡片
已保存数据的展示区使用浅灰色背景 #f8f9fa + 圆角 8,与白色卡片背景形成对比,视觉上突出数据内容。这种「卡片中的卡片」设计在表单类应用中很常见。
5.3 标签 + 输入框分组
每个输入字段都使用「标签在上、输入框在下」的垂直分组布局,用 Column({ space: 6 }) 控制标签和输入框的间距。这种布局比水平排列更适合移动端,标签不会被截断。
6. 运行与测试
6.1 运行步骤
- 使用 DevEco Studio 打开项目。
- 运行项目到模拟器或真机。
- 在首页找到「数据存储」示例入口,点击进入。
- 观察页面初始状态(应显示「暂无已保存的数据」)。
- 输入姓名和年龄,点击「保存」。
- 观察「已保存的数据」区域更新。
- 点击「清除」,确认数据被删除。
- 重新保存数据,关闭应用后重新打开,确认数据仍在。
6.2 测试场景
| 测试场景 | 预期结果 |
|---|---|
| 首次进入页面 | 展示区显示「暂无已保存的数据」 |
| 输入姓名「张三」年龄「25」点击保存 | Toast「已保存」,展示区显示「姓名:张三 年龄:25」 |
| 点击「读取」 | 展示区显示当前保存的数据 |
| 点击「清除」 | Toast「已清除」,输入框清空,展示区显示「暂无已保存的数据」 |
| 保存后退出应用重新进入 | 展示区自动显示上次保存的数据 |
| 年龄输入框 | 只接受数字输入,弹出数字键盘 |
| 不输入任何内容点击保存 | 保存空串,展示区显示「姓名: 年龄:」 |
7. 可扩展方向
7.1 异步 API
将同步 API 替换为异步 API,避免在主线程上执行 IO 操作:
const pref = await preferences.getPreferences(getContext(this), { name: 'userStore' });
await pref.put('name', this.name);
await pref.flush();
异步 API 适用于大数据量或频繁写入的场景。
7.2 数据加密
对于敏感数据(如密码、token),可以在存储前加密,读取后解密。HarmonyOS 提供了 @kit.CryptoArchitectureKit 加密模块。
7.3 复杂数据结构
Preferences 支持 Uint8Array 类型,可以存储序列化后的 JSON 对象:
const user = JSON.stringify({ name: '张三', age: 25, hobbies: ['阅读', '游泳'] });
pref.putSync('user', user);
const data = JSON.parse(pref.getSync('user', '{}') as string);
7.4 数据迁移
在应用版本更新时,可能需要迁移 Preferences 数据结构。可以在 aboutToAppear 中检查版本号,执行数据迁移逻辑。
7.5 多实例管理
使用不同的 name 创建多个 Preferences 实例,按模块隔离数据:
const userPref = preferences.getPreferencesSync(getContext(this), { name: 'userStore' });
const settingPref = preferences.getPreferencesSync(getContext(this), { name: 'settingStore' });
8. 常见问题与调试
8.1 数据未持久化
问题:保存数据后重启应用,数据丢失。
排查:
- 确认
save()方法中调用了pref.flush()。putSync只写入内存,flush才写入磁盘。 - 检查
getPreferencesSync的name参数在保存和读取时是否一致。不同 name 对应不同文件。
8.2 getSync 返回类型错误
问题:getSync 返回的值无法赋值给 string 变量,编译报错。
排查:
- 确认使用了
as string类型断言。getSync返回ValueType类型,需要断言为具体类型。 - 检查存储时的类型与读取时的类型是否一致。存入
string就应该断言为string。
8.3 读取数据为空
问题:保存后读取,但返回默认值(空串)。
排查:
- 确认
putSync和getSync使用了相同的 key。如putSync('name', ...)和getSync('name', ...)。 - 检查
flush()是否在putSync之后调用。 - 确认
getPreferencesSync的name参数一致。
8.4 清除后仍能读到数据
问题:点击「清除」后,读取仍然返回旧数据。
排查:
- 确认
clearData()方法中调用了flush()。deleteSync只在内存中删除,flush才同步到磁盘。 - 检查
deleteSync的 key 是否正确。
8.5 操作崩溃
问题:点击保存/读取/清除时应用崩溃。
排查:
- 确认所有操作都包裹在
try-catch中。 - 检查
getContext(this)是否在正确的上下文中调用。在组件方法中调用是安全的,在独立函数中可能获取不到 context。 - 查看 DevEco Studio 的日志输出,定位具体的异常信息。
9. 技术总结
示例 95 的数据存储展示了 Preferences API 的完整增删改查流程。通过这个示例,我们可以总结出 Preferences 使用的几个关键范式:
- 同步 API 三件套:
putSync写入、getSync读取、deleteSync删除,配合flush持久化,是最简洁的数据存储模式。 - getSync 默认值:
getSync(key, defaultValue)的第二个参数指定默认值,当 key 不存在时返回该值,避免 null 异常。 - 类型断言:
getSync返回ValueType,需要用as断言为具体类型,满足 ArkTS 的静态类型要求。 - try-catch 保护:所有 IO 操作都应包裹在
try-catch中,防止异常导致应用崩溃。 - aboutToAppear 自动加载:在页面创建时自动加载已保存的数据,提供无缝的用户体验。
- flush 必须调用:
putSync和deleteSync只修改内存缓存,flush才将更改写入磁盘。
掌握了这些范式后,就可以轻松地将 Preferences 应用到用户设置持久化、表单草稿保存、应用首次启动标记等各种本地存储场景中。Preferences 作为 HarmonyOS 最轻量级的数据存储方案,是每个鸿蒙开发者必须掌握的基础技能。
更多推荐



所有评论(0)