在这里插入图片描述

引言

数据持久化是应用开发中不可回避的核心主题。无论是记住用户的登录状态、保存应用的配置偏好、还是缓存最近浏览的记录,都需要将数据存储到设备的本地存储中,确保应用关闭后数据不会丢失。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 同时提供同步版本(putSyncgetSync 等)和异步版本(putget 等返回 Promise),本示例使用同步版本简化代码。

2.2 getPreferencesSync 获取实例

使用 Preferences 的第一步是获取实例:

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

const pref: preferences.Preferences = preferences.getPreferencesSync(getContext(this), { name: 'userStore' });

getPreferencesSync 接收两个参数:

  1. context:应用上下文,通过 getContext(this) 获取。Preferences 文件存储在应用沙箱目录下,需要 context 来确定存储路径。
  2. 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,这是因为 TextInputonChange 回调返回的是字符串,直接用字符串存储更方便。需要数值运算时再通过 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 方法的执行流程:

  1. 获取实例getPreferencesSync 获取名为 userStore 的 Preferences 实例。
  2. 写入数据putSync('name', this.name)putSync('age', this.age) 将姓名和年龄写入内存缓存。
  3. 刷盘持久化flush() 将内存数据写入磁盘文件。
  4. 提示成功showToast 弹出「已保存」提示。
  5. 刷新展示:调用 this.load() 重新读取数据,更新「已保存的数据」展示区。
  6. 异常处理:如果任何步骤出错,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 方法的执行流程:

  1. 获取实例:与 save 方法使用相同的 name: 'userStore',确保读写同一个存储实例。
  2. 读取数据getSync('name', '') 读取姓名,如果 key 不存在返回空串。as string 进行类型断言。
  3. 格式化展示:如果姓名或年龄任一不为空,拼接为「姓名:XXX 年龄:YYY」;都为空则显示「暂无已保存的数据」。
  4. 异常处理:出错时设置 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 方法的执行流程:

  1. 获取实例:同样使用 userStore 实例。
  2. 删除数据deleteSync('name')deleteSync('age') 分别删除姓名和年龄的键值对。
  3. 刷盘持久化flush() 确保删除操作写入磁盘。
  4. 清空输入框this.name = ''this.age = '' 清空输入框内容。
  5. 刷新展示:调用 this.load() 更新展示区,此时会显示「暂无已保存的数据」。
  6. 提示成功:弹出「已清除」提示。
  7. 异常处理:出错时弹出「清除失败」。

清除操作不仅删除存储中的数据,还清空了输入框,让页面恢复到初始状态。

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 })

主操作卡片包含以下部分:

  1. 标题说明:「Preferences 本地键值存储」灰色小字说明功能。
  2. 姓名输入区:标签 + TextInputtext: this.name 双向绑定。
  3. 年龄输入区:标签 + TextInputtype: InputType.Number 限制只能输入数字。
  4. 操作按钮行:三个按钮等宽排列(layoutWeight(1)),颜色区分功能——蓝色保存、橙色读取、红色清除。
  5. 已保存数据展示:灰色背景圆角卡片,显示已保存的数据或提示文字。

InputType.Number 是一个重要的细节——它让年龄输入框只接受数字输入,自动弹出数字键盘,避免用户输入非数字字符。

3.10 底部提示

Text('应用重启后数据仍然保留,试试重新进入本页')
  .fontSize(12)
  .fontColor('#999999')
  .margin({ top: 14 })

Blank()

底部提示引导用户验证数据持久化效果。Blank() 在底部占据剩余空间,将提示文字推到合适的位置。

4. 交互流程详解

4.1 首次进入页面

  1. aboutToAppear 生命周期触发,调用 load() 方法。
  2. getPreferencesSync 获取 userStore 实例(如果文件不存在,会自动创建)。
  3. getSync('name', '') 返回默认值空串(因为还没有保存过数据)。
  4. getSync('age', '') 同样返回空串。
  5. this.saved 设为「暂无已保存的数据」。
  6. 页面渲染,显示空输入框和「暂无已保存的数据」。

4.2 保存数据

用户输入姓名「张三」和年龄「25」,点击「保存」:

  1. save() 方法被调用。
  2. putSync('name', '张三') 将姓名写入内存缓存。
  3. putSync('age', '25') 将年龄写入内存缓存。
  4. flush() 将内存数据写入磁盘文件。
  5. Toast 弹出「已保存」。
  6. this.load() 被调用,重新读取数据。
  7. this.saved 更新为「姓名:张三 年龄:25」。
  8. 展示区刷新,显示新保存的数据。

4.3 读取数据

用户点击「读取」按钮:

  1. load() 方法被调用。
  2. getSync('name', '') 返回「张三」。
  3. getSync('age', '') 返回「25」。
  4. this.saved 更新为「姓名:张三 年龄:25」。

读取操作总是反映磁盘上的最新数据,与输入框中的内容无关。

4.4 清除数据

用户点击「清除」按钮:

  1. clearData() 方法被调用。
  2. deleteSync('name') 删除 name 键。
  3. deleteSync('age') 删除 age 键。
  4. flush() 将删除操作写入磁盘。
  5. this.name = ''this.age = '' 清空输入框。
  6. this.load() 被调用,getSync 返回默认值空串。
  7. this.saved 更新为「暂无已保存的数据」。
  8. Toast 弹出「已清除」。

4.5 应用重启后验证

用户保存数据后关闭应用,重新打开:

  1. aboutToAppear 触发,调用 load()
  2. getSync 从磁盘文件读取上次保存的数据。
  3. 展示区显示「姓名:张三 年龄:25」。

这就是数据持久化的核心价值——数据在应用关闭后仍然保留。

5. UI 样式设计思路

5.1 三色按钮区分功能

三个操作按钮使用不同颜色:

  • 蓝色 #1a6cff(保存):主题色,表示主要操作。
  • 橙色 #ff8f1f(读取):暖色,表示查询操作。
  • 红色 #ff4d4f(清除):警示色,表示危险操作。

三色按钮让用户一眼就能区分不同功能,降低误操作风险。

5.2 数据展示卡片

已保存数据的展示区使用浅灰色背景 #f8f9fa + 圆角 8,与白色卡片背景形成对比,视觉上突出数据内容。这种「卡片中的卡片」设计在表单类应用中很常见。

5.3 标签 + 输入框分组

每个输入字段都使用「标签在上、输入框在下」的垂直分组布局,用 Column({ space: 6 }) 控制标签和输入框的间距。这种布局比水平排列更适合移动端,标签不会被截断。

6. 运行与测试

6.1 运行步骤

  1. 使用 DevEco Studio 打开项目。
  2. 运行项目到模拟器或真机。
  3. 在首页找到「数据存储」示例入口,点击进入。
  4. 观察页面初始状态(应显示「暂无已保存的数据」)。
  5. 输入姓名和年龄,点击「保存」。
  6. 观察「已保存的数据」区域更新。
  7. 点击「清除」,确认数据被删除。
  8. 重新保存数据,关闭应用后重新打开,确认数据仍在。

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 才写入磁盘。
  • 检查 getPreferencesSyncname 参数在保存和读取时是否一致。不同 name 对应不同文件。

8.2 getSync 返回类型错误

问题getSync 返回的值无法赋值给 string 变量,编译报错。

排查

  • 确认使用了 as string 类型断言。getSync 返回 ValueType 类型,需要断言为具体类型。
  • 检查存储时的类型与读取时的类型是否一致。存入 string 就应该断言为 string

8.3 读取数据为空

问题:保存后读取,但返回默认值(空串)。

排查

  • 确认 putSyncgetSync 使用了相同的 key。如 putSync('name', ...)getSync('name', ...)
  • 检查 flush() 是否在 putSync 之后调用。
  • 确认 getPreferencesSyncname 参数一致。

8.4 清除后仍能读到数据

问题:点击「清除」后,读取仍然返回旧数据。

排查

  • 确认 clearData() 方法中调用了 flush()deleteSync 只在内存中删除,flush 才同步到磁盘。
  • 检查 deleteSync 的 key 是否正确。

8.5 操作崩溃

问题:点击保存/读取/清除时应用崩溃。

排查

  • 确认所有操作都包裹在 try-catch 中。
  • 检查 getContext(this) 是否在正确的上下文中调用。在组件方法中调用是安全的,在独立函数中可能获取不到 context。
  • 查看 DevEco Studio 的日志输出,定位具体的异常信息。

9. 技术总结

示例 95 的数据存储展示了 Preferences API 的完整增删改查流程。通过这个示例,我们可以总结出 Preferences 使用的几个关键范式:

  1. 同步 API 三件套putSync 写入、getSync 读取、deleteSync 删除,配合 flush 持久化,是最简洁的数据存储模式。
  2. getSync 默认值getSync(key, defaultValue) 的第二个参数指定默认值,当 key 不存在时返回该值,避免 null 异常。
  3. 类型断言getSync 返回 ValueType,需要用 as 断言为具体类型,满足 ArkTS 的静态类型要求。
  4. try-catch 保护:所有 IO 操作都应包裹在 try-catch 中,防止异常导致应用崩溃。
  5. aboutToAppear 自动加载:在页面创建时自动加载已保存的数据,提供无缝的用户体验。
  6. flush 必须调用putSyncdeleteSync 只修改内存缓存,flush 才将更改写入磁盘。

掌握了这些范式后,就可以轻松地将 Preferences 应用到用户设置持久化、表单草稿保存、应用首次启动标记等各种本地存储场景中。Preferences 作为 HarmonyOS 最轻量级的数据存储方案,是每个鸿蒙开发者必须掌握的基础技能。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐