鸿蒙 ArkTS 数据持久化实战:AppStorage、用户首选项与分布式数据管理

前言

做过鸿蒙应用开发的同学都知道,数据持久化是几乎每个 App 都绕不开的基础能力。用户登录状态、主题偏好、表单草稿、网络配置……这些数据在应用重启后必须能恢复,否则用户体验会大打折扣。

鸿蒙 Next 提供了三套持久化机制:AppStorage(应用全局状态)、用户首选项(Userinfo Preference)(轻量键值对)、分布式数据管理(跨设备同步)。很多人把它们混着用,结果踩坑无数——AppStorage 用来存密码导致数据丢失,Userinfo Preference 用来存复杂对象导致序列化失败。

本文以登录状态 + 主题偏好 + 表单草稿三个真实场景为例,完整讲解三套机制的使用场景、API 用法、踩坑总结,以及如何封装一套易用的数据管理工具类。

阅读前提:熟悉 ArkTS 基础语法,了解 HarmonyOS 应用工程结构。


一、三套持久化机制全景对比

特性 AppStorage 用户首选项(Userinfo Preference) 持久化 Storage
存储位置 进程内存 + App 级别持久化 独立文件(JSON 格式) 独立文件
数据类型 任意类型(支持复杂对象) string/number/boolean 任意类型
响应式 ✅ 支持 @StorageLink/@StorageProp ❌ 需要手动订阅 ❌ 需要手动订阅
生命周期 随 App 进程 持久化,App 重启保留 持久化
跨设备同步 ✅(分布式 Storage)
适用场景 UI 状态全局共享 配置项、开关、简单偏好 复杂数据、跨设备
数据容量 小(< 2MB 推荐) 中(< 1MB 推荐)

决策树

  • 需要响应式更新 UI?→ AppStorage
  • 需要 App 重启后保留但不需要响应式?→ 用户首选项
  • 需要跨设备同步或存大量数据?→ 持久化 Storage

二、AppStorage:应用全局状态(带响应式)

2.1 核心 API

// 存值
AppStorage.setOrCreate('username', 'bingo')
AppStorage.setOrCreate('theme', 'dark')
AppStorage.setOrCreate('userProfile', { id: 1, name: '斌哥', avatar: '' }) // 对象自动序列化

// 取值
const username: string = AppStorage.get<string>('username') ?? ''

// 删除
AppStorage.delete('username')

// 是否存在
const hasKey: boolean = AppStorage.has('username')

2.2 在组件中响应式使用

AppStorage 的精髓在于和 @StorageLink / @StorageProp 装饰器配合,实现状态驱动 UI 更新

// 主题管理工具
export class ThemeManager {
  static setTheme(mode: 'light' | 'dark' | 'system') {
    AppStorage.setOrCreate('themeMode', mode)
    // 同步更新全局主题色
    AppStorage.setOrCreate('primaryColor', mode === 'dark' ? '#1A1A1A' : '#FFFFFF')
  }

  static getTheme(): string {
    return AppStorage.get<string>('themeMode') ?? 'system'
  }
}

// 自定义导航栏组件(响应式)
@StorageLink('primaryColor') primaryColor: string = '#FFFFFF'

@Component
struct CustomNavBar {
  build() {
    Row() {
      Text('我的应用')
        .fontColor(this.primaryColor === '#1A1A1A' ? '#FFFFFF' : '#000000')
        .fontSize(20)
        .fontWeight(FontWeight.Bold)
    }
    .width('100%')
    .height(56)
    .backgroundColor(this.primaryColor)
  }
}

踩坑:AppStorage 虽然支持复杂对象,但对象更新时必须整体替换,不能直接修改对象属性触发更新:

// ❌ 错误:对象引用未变,UI 不会更新
const profile = AppStorage.get<object>('userProfile')
if (profile) (profile as any).name = '新名字'

// ✅ 正确:创建新对象替换
const oldProfile = AppStorage.get<object>('userProfile') ?? {}
const newProfile = { ...oldProfile as object, name: '新名字' }
AppStorage.setOrCreate('userProfile', newProfile)

三、用户首选项(Userinfo Preference):轻量键值持久化

3.1 初始化与读写

用户首选项以文件形式存储在 data/app/ 目录下,推荐在 Ability 启动时初始化一次,后续全局复用。

import dataPreferences from '@ohos.data.preferences'

// 初始化首选项实例
let preference: dataPreferences.Preferences | null = null

export async function initPreference(context: Context): Promise<void> {
  preference = await dataPreferences.getPreferences(context, 'user_prefs')
}

// ==================== 基础 CRUD ====================

export async function setPreference(key: string, value: string | number | boolean): Promise<void> {
  if (!preference) throw new Error('Preference not initialized')
  await preference.put(key, value)
  await preference.flush() // 关键:必须 flush 才真正写入磁盘
}

export async function getPreference<T>(key: string, defaultValue: T): Promise<T> {
  if (!preference) throw new Error('Preference not initialized')
  return await preference.get<T>(key, defaultValue)
}

export async function deletePreference(key: string): Promise<void> {
  if (!preference) throw new Error('Preference not initialized')
  await preference.delete(key)
  await preference.flush()
}

// ==================== 常用场景封装 ====================

// 保存登录 Token
export async function saveToken(token: string): Promise<void> {
  await setPreference('auth_token', token)
}

// 获取登录 Token
export async function getToken(): Promise<string> {
  return await getPreference<string>('auth_token', '')
}

// 清除登录态
export async function clearAuth(): Promise<void> {
  await deletePreference('auth_token')
  await deletePreference('user_id')
  await deletePreference('user_profile')
}

// 保存表单草稿(JSON 序列化)
export async function saveFormDraft(formData: object): Promise<void> {
  const json = JSON.stringify(formData)
  await setPreference('form_draft', json)
}

// 读取表单草稿
export async function getFormDraft<T>(): Promise<T | null> {
  const json = await getPreference<string>('form_draft', '')
  if (!json) return null
  try {
    return JSON.parse(json) as T
  } catch {
    return null
  }
}

3.2 订阅数据变化(监听首选项更新)

// 监听 'theme' 键值变化
preference.on('change', (newValue: dataPreferences.DataValidator) => {
  console.info('Theme changed to:', newValue)
})

// 应用退出时删除监听器,避免内存泄漏
preference.off('change', callback)

四、实战场景:登录状态全流程管理

以一个完整的登录/登出流程为例,演示三套存储如何协同工作:

4.1 登录状态管理模块

// src/utils/authStorage.ets

import { setPreference, getPreference, deletePreference } from './preferenceHelper'
import { AppStorage } from '@ohos.app.ability.Window'

export interface UserInfo {
  id: string
  username: string
  avatar: string
  role: string
}

// 是否已登录(AppStorage 响应式,供 UI 直接使用)
@StorageLink('isLoggedIn') isLoggedIn: boolean = false
@StorageLink('currentUser') currentUser: UserInfo | null = null

export async function login(userInfo: UserInfo, token: string): Promise<void> {
  // 1. 持久化存储(重启后恢复)
  await setPreference('auth_token', token)
  await setPreference('user_id', userInfo.id)
  await setPreference('user_profile', JSON.stringify(userInfo))

  // 2. AppStorage 响应式更新(当前会话内 UI 自动刷新)
  AppStorage.setOrCreate('isLoggedIn', true)
  AppStorage.setOrCreate('currentUser', userInfo)
  AppStorage.setOrCreate('authToken', token)
}

export async function logout(): Promise<void> {
  // 清除持久化
  await deletePreference('auth_token')
  await deletePreference('user_id')
  await deletePreference('user_profile')

  // 清除 AppStorage
  AppStorage.delete('isLoggedIn')
  AppStorage.delete('currentUser')
  AppStorage.delete('authToken')

  // UI 自动更新(isLoggedIn 变 false)
}

// 应用冷启动时恢复登录态
export async function restoreLoginState(): Promise<boolean> {
  const token = await getPreference<string>('auth_token', '')
  const userId = await getPreference<string>('user_id', '')
  const profileJson = await getPreference<string>('user_profile', '')

  if (token && userId && profileJson) {
    try {
      const userInfo: UserInfo = JSON.parse(profileJson)
      AppStorage.setOrCreate('isLoggedIn', true)
      AppStorage.setOrCreate('currentUser', userInfo)
      AppStorage.setOrCreate('authToken', token)
      return true
    } catch {
      // 数据损坏,清除并重新登录
      await logout()
      return false
    }
  }
  return false
}

4.2 启动时恢复登录态(EntryAbility)

// EntryAbility.ets
import { restoreLoginState } from '../utils/authStorage'

onCreate(want, launchParam) {
  hilog.info(0x0000, 'EntryAbility', 'Application onCreate')

  // 初始化首选项
  initPreference(this.context)

  // 恢复登录态
  restoreLoginState().then(isLoggedIn => {
    hilog.info(0x0000, 'EntryAbility', `Login state restored: ${isLoggedIn}`)
  })
}

4.3 登录页面使用示例

// LoginPage.ets
import { login } from '../utils/authStorage'
import { AppStorage } from '@ohos.app.ability.Window'

@Entry
@Component
struct LoginPage {
  @State username: string = ''
  @State password: string = ''

  async handleLogin() {
    if (!this.username || !this.password) {
      promptAction.showToast({ message: '请输入用户名和密码' })
      return
    }

    // 模拟登录 API 调用
    const result = await this.callLoginApi(this.username, this.password)
    if (result.success) {
      await login(result.userInfo, result.token)
      // 跳转到主页
      router.replaceUrl({ url: 'pages/MainPage' })
    }
  }

  build() {
    Column({ space: 20 }) {
      Text('登录')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)

      TextInput({ placeholder: '用户名', text: this.username })
        .onChange(v => this.username = v)

      TextInput({ placeholder: '密码', text: this.password })
        .type(InputType.Password)
        .onChange(v => this.password = v)

      Button('登录')
        .onClick(() => this.handleLogin())
    }
    .padding(24)
  }
}

五、主题偏好管理封装

// src/utils/themeStorage.ets

export type ThemeMode = 'light' | 'dark' | 'system'
export type AccentColor = 'blue' | 'green' | 'purple' | 'orange'

const THEME_KEY = 'app_theme_mode'
const ACCENT_KEY = 'app_accent_color'

export class ThemeStorage {
  // 保存主题模式
  static async setTheme(mode: ThemeMode): Promise<void> {
    await setPreference(THEME_KEY, mode)
    AppStorage.setOrCreate('themeMode', mode)
    this.applyTheme(mode)
  }

  // 读取主题模式
  static async getTheme(): Promise<ThemeMode> {
    const saved = await getPreference<number>(THEME_KEY, 2) // 0=light,1=dark,2=system
    const modeMap: ThemeMode[] = ['light', 'dark', 'system']
    const mode = modeMap[saved] ?? 'system'
    AppStorage.setOrCreate('themeMode', mode)
    return mode
  }

  // 应用主题(更新 Window 主题)
  static applyTheme(mode: ThemeMode): void {
    const colorScheme = mode === 'light' ?
      (colorScheme: ColorScheme) => ColorScheme.Light :
      mode === 'dark' ? ColorScheme.Dark : ColorScheme.Light

    // 获取当前 window 对象并设置主题
    window.getLastWindow(this.getContext())
      .then(win => win.setColorScheme(
        mode === 'light' ? window.ColorScheme.Light : window.ColorScheme.Dark
      ))
  }

  // 初始化(启动时调用)
  static async init(context: Context): Promise<void> {
    await initPreference(context)
    const mode = await this.getTheme()
    this.applyTheme(mode)
  }
}

六、踩坑总结表

场景 错误做法 正确做法 原因
存储 Token AppStorage 只存内存 AppStorage + Userinfo Preference 双写 AppStorage 进程重启丢失
存复杂对象 直接 obj.prop = val AppStorage.setOrCreate('key', {...obj, prop: val}) AppStorage 基于引用比较
读首选项 组件内直接 getPreference() aboutToAppear 异步读取,同步值存 AppStorage 同步渲染需要预加载
删除首选项 只调用 delete() 调用 delete() + flush() delete 只是标记,flush 才落盘
存布尔值 preference.put('flag', 1) preference.put('flag', false) 类型不一致导致读取类型推断错误
多实例竞争 多个 Ability 分别 getPreferences 统一模块导出单例 preference 对象 多文件同时 flush 导致数据覆盖

七、一句话总结

  • AppStorage:全局响应式状态,进程内共享,适合 UI 联动
  • 用户首选项:轻量键值持久化,适合配置项、Token、草稿
  • 持久化 Storage:复杂对象、大数据量、跨设备同步场景
  • 核心原则:Token 和用户信息永远双写(AppStorage + Preference),避免单一存储的丢失风险

参考资料

  • HarmonyOS Developer 官方文档:应用数据管理
  • ArkTS 装饰器完整指南(@StorageLink / @StorageProp)
  • HarmonyOS OpenHarmony dataPreferences API 文档

关注我,获取更多鸿蒙开发实战技巧。有问题欢迎在评论区交流!

Logo

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

更多推荐