HarmonyOS APP<玩转React>开源教程七:HarmonyOS 数据存储方案

第7次:本地数据持久化

数据持久化让应用能够保存用户数据,即使应用关闭后再次打开,数据依然存在。本次课程将深入学习 HarmonyOS 的 Preferences 存储机制,完善 StorageUtil 工具类。


学习目标

  • 理解 HarmonyOS 数据存储方案
  • 掌握 Preferences 轻量级存储
  • 完善 StorageUtil 工具类
  • 学会异步存储操作
  • 了解数据迁移与版本管理

7.1 HarmonyOS 数据存储方案

存储方案对比

方案 适用场景 数据量 特点
Preferences 配置、设置 键值对,轻量快速
关系型数据库 结构化数据 SQL 查询,事务支持
文件存储 文件、图片 灵活,适合二进制
分布式数据 跨设备同步 多设备协同

本项目选择 Preferences

React 学习教程 App 的数据特点:

  • 用户进度:结构简单,数据量小
  • 收藏列表:数组形式,更新频繁
  • 主题设置:单个配置项

Preferences 完全满足需求,且使用简单、性能优秀。


7.2 Preferences 存储详解

导入模块

typescript 复制代码
import { preferences } from '@kit.ArkData';

获取 Preferences 实例

typescript 复制代码
// 获取或创建 Preferences 实例
const prefs = await preferences.getPreferences(context, 'my_preferences');

基本操作

typescript 复制代码
// 写入数据
await prefs.put('key', 'value');
await prefs.flush();  // 持久化到磁盘

// 读取数据
const value = await prefs.get('key', 'defaultValue');

// 检查键是否存在
const hasKey = await prefs.has('key');

// 删除键
await prefs.delete('key');
await prefs.flush();

// 清空所有数据
await prefs.clear();
await prefs.flush();

支持的数据类型

typescript 复制代码
// 字符串
await prefs.put('name', '张三');

// 数字
await prefs.put('age', 25);

// 布尔值
await prefs.put('isVip', true);

// 数字数组
await prefs.put('scores', [90, 85, 92]);

// 字符串数组
await prefs.put('tags', ['react', 'hooks']);

// 注意:不直接支持对象,需要 JSON 序列化
await prefs.put('user', JSON.stringify({ name: '张三', age: 25 }));

7.3 完善 StorageUtil 工具类

完整实现

更新 entry/src/main/ets/common/StorageUtil.ets

typescript 复制代码
/**
 * 持久化存储工具类
 * 基于 Preferences 实现本地数据存储
 */
import { preferences } from '@kit.ArkData';
import { AppConstants } from './Constants';

/**
 * 存储工具类
 */
export class StorageUtil {
  private static preferencesInstance: preferences.Preferences | null = null;
  private static isInitialized: boolean = false;

  /**
   * 初始化 Preferences
   * @param context 应用上下文
   */
  static async init(context: Context): Promise<void> {
    if (StorageUtil.isInitialized) {
      console.info('[StorageUtil] Already initialized');
      return;
    }

    try {
      StorageUtil.preferencesInstance = await preferences.getPreferences(
        context,
        AppConstants.PREFERENCES_NAME
      );
      StorageUtil.isInitialized = true;
      console.info('[StorageUtil] Preferences initialized successfully');
    } catch (error) {
      console.error('[StorageUtil] Failed to init preferences:', error);
      throw error;
    }
  }

  /**
   * 检查是否已初始化
   */
  static checkInitialized(): void {
    if (!StorageUtil.isInitialized || !StorageUtil.preferencesInstance) {
      throw new Error('[StorageUtil] Not initialized. Call init() first.');
    }
  }

  /**
   * 获取 Preferences 实例
   */
  private static getPreferences(): preferences.Preferences {
    StorageUtil.checkInitialized();
    return StorageUtil.preferencesInstance!;
  }

  // ==================== 字符串操作 ====================

  /**
   * 获取字符串值
   */
  static async getString(key: string, defaultValue: string = ''): Promise<string> {
    try {
      const prefs = StorageUtil.getPreferences();
      const value = await prefs.get(key, defaultValue);
      return value as string;
    } catch (error) {
      console.error(`[StorageUtil] getString failed for key "${key}":`, error);
      return defaultValue;
    }
  }

  /**
   * 设置字符串值
   */
  static async setString(key: string, value: string): Promise<boolean> {
    try {
      const prefs = StorageUtil.getPreferences();
      await prefs.put(key, value);
      await prefs.flush();
      return true;
    } catch (error) {
      console.error(`[StorageUtil] setString failed for key "${key}":`, error);
      return false;
    }
  }

  // ==================== 数字操作 ====================

  /**
   * 获取数字值
   */
  static async getNumber(key: string, defaultValue: number = 0): Promise<number> {
    try {
      const prefs = StorageUtil.getPreferences();
      const value = await prefs.get(key, defaultValue);
      return value as number;
    } catch (error) {
      console.error(`[StorageUtil] getNumber failed for key "${key}":`, error);
      return defaultValue;
    }
  }

  /**
   * 设置数字值
   */
  static async setNumber(key: string, value: number): Promise<boolean> {
    try {
      const prefs = StorageUtil.getPreferences();
      await prefs.put(key, value);
      await prefs.flush();
      return true;
    } catch (error) {
      console.error(`[StorageUtil] setNumber failed for key "${key}":`, error);
      return false;
    }
  }

  // ==================== 布尔值操作 ====================

  /**
   * 获取布尔值
   */
  static async getBoolean(key: string, defaultValue: boolean = false): Promise<boolean> {
    try {
      const prefs = StorageUtil.getPreferences();
      const value = await prefs.get(key, defaultValue);
      return value as boolean;
    } catch (error) {
      console.error(`[StorageUtil] getBoolean failed for key "${key}":`, error);
      return defaultValue;
    }
  }

  /**
   * 设置布尔值
   */
  static async setBoolean(key: string, value: boolean): Promise<boolean> {
    try {
      const prefs = StorageUtil.getPreferences();
      await prefs.put(key, value);
      await prefs.flush();
      return true;
    } catch (error) {
      console.error(`[StorageUtil] setBoolean failed for key "${key}":`, error);
      return false;
    }
  }

  // ==================== 对象操作 ====================

  /**
   * 获取对象(JSON 反序列化)
   */
  static async getObject<T>(key: string, defaultValue: T): Promise<T> {
    try {
      const prefs = StorageUtil.getPreferences();
      const jsonStr = await prefs.get(key, '') as string;

      if (!jsonStr || jsonStr === '') {
        return defaultValue;
      }

      const parsed = JSON.parse(jsonStr) as T;

      // 验证解析结果
      if (parsed === null || parsed === undefined) {
        return defaultValue;
      }

      return parsed;
    } catch (error) {
      console.error(`[StorageUtil] getObject failed for key "${key}":`, error);
      return defaultValue;
    }
  }

  /**
   * 设置对象(JSON 序列化)
   */
  static async setObject<T>(key: string, value: T): Promise<boolean> {
    try {
      const prefs = StorageUtil.getPreferences();
      const jsonStr = JSON.stringify(value);
      await prefs.put(key, jsonStr);
      await prefs.flush();
      return true;
    } catch (error) {
      console.error(`[StorageUtil] setObject failed for key "${key}":`, error);
      return false;
    }
  }

  // ==================== 数组操作 ====================

  /**
   * 获取字符串数组
   */
  static async getStringArray(key: string, defaultValue: string[] = []): Promise<string[]> {
    return StorageUtil.getObject<string[]>(key, defaultValue);
  }

  /**
   * 设置字符串数组
   */
  static async setStringArray(key: string, value: string[]): Promise<boolean> {
    return StorageUtil.setObject(key, value);
  }

  /**
   * 向数组添加元素(去重)
   */
  static async addToArray(key: string, item: string): Promise<boolean> {
    try {
      const arr = await StorageUtil.getStringArray(key);
      if (!arr.includes(item)) {
        arr.push(item);
        return StorageUtil.setStringArray(key, arr);
      }
      return true;
    } catch (error) {
      console.error(`[StorageUtil] addToArray failed for key "${key}":`, error);
      return false;
    }
  }

  /**
   * 从数组移除元素
   */
  static async removeFromArray(key: string, item: string): Promise<boolean> {
    try {
      const arr = await StorageUtil.getStringArray(key);
      const index = arr.indexOf(item);
      if (index > -1) {
        arr.splice(index, 1);
        return StorageUtil.setStringArray(key, arr);
      }
      return true;
    } catch (error) {
      console.error(`[StorageUtil] removeFromArray failed for key "${key}":`, error);
      return false;
    }
  }

  // ==================== 通用操作 ====================

  /**
   * 检查键是否存在
   */
  static async has(key: string): Promise<boolean> {
    try {
      const prefs = StorageUtil.getPreferences();
      return await prefs.has(key);
    } catch (error) {
      console.error(`[StorageUtil] has failed for key "${key}":`, error);
      return false;
    }
  }

  /**
   * 删除指定键
   */
  static async remove(key: string): Promise<boolean> {
    try {
      const prefs = StorageUtil.getPreferences();
      await prefs.delete(key);
      await prefs.flush();
      return true;
    } catch (error) {
      console.error(`[StorageUtil] remove failed for key "${key}":`, error);
      return false;
    }
  }

  /**
   * 清空所有数据
   */
  static async clear(): Promise<boolean> {
    try {
      const prefs = StorageUtil.getPreferences();
      await prefs.clear();
      await prefs.flush();
      console.info('[StorageUtil] All data cleared');
      return true;
    } catch (error) {
      console.error('[StorageUtil] clear failed:', error);
      return false;
    }
  }

  /**
   * 获取所有键
   */
  static async getAllKeys(): Promise<string[]> {
    try {
      const prefs = StorageUtil.getPreferences();
      // 注意:Preferences 没有直接获取所有键的方法
      // 这里返回空数组,实际项目中可以维护一个键列表
      return [];
    } catch (error) {
      console.error('[StorageUtil] getAllKeys failed:', error);
      return [];
    }
  }
}

7.4 异步存储操作

理解异步操作

Preferences 的所有操作都是异步的,需要使用 async/await 或 Promise:

typescript 复制代码
// 方式一:async/await(推荐)
async function saveUserData() {
  await StorageUtil.setString('name', '张三');
  await StorageUtil.setNumber('age', 25);
  console.log('保存完成');
}

// 方式二:Promise
function saveUserData() {
  StorageUtil.setString('name', '张三')
    .then(() => StorageUtil.setNumber('age', 25))
    .then(() => console.log('保存完成'))
    .catch(error => console.error('保存失败', error));
}

批量操作优化

typescript 复制代码
// 不推荐:多次 flush
async function saveMultiple() {
  await StorageUtil.setString('key1', 'value1');  // flush
  await StorageUtil.setString('key2', 'value2');  // flush
  await StorageUtil.setString('key3', 'value3');  // flush
}

// 推荐:合并为一个对象
async function saveMultiple() {
  const data = {
    key1: 'value1',
    key2: 'value2',
    key3: 'value3'
  };
  await StorageUtil.setObject('batchData', data);  // 只 flush 一次
}

错误处理

typescript 复制代码
async function loadUserProgress(): Promise<UserProgress> {
  try {
    const progress = await StorageUtil.getObject<UserProgress>(
      StorageKeys.USER_PROGRESS,
      DEFAULT_USER_PROGRESS
    );
    return progress;
  } catch (error) {
    console.error('加载进度失败:', error);
    // 返回默认值,保证应用正常运行
    return DEFAULT_USER_PROGRESS;
  }
}

7.5 数据迁移与版本管理

为什么需要数据迁移?

当应用更新时,数据结构可能发生变化:

  • 新增字段
  • 删除字段
  • 修改字段类型
  • 重命名字段

版本管理策略

typescript 复制代码
/**
 * 数据版本管理
 */
export class DataMigration {
  private static readonly CURRENT_VERSION = 2;
  private static readonly VERSION_KEY = 'data_version';

  /**
   * 检查并执行数据迁移
   */
  static async checkAndMigrate(): Promise<void> {
    const storedVersion = await StorageUtil.getNumber(
      DataMigration.VERSION_KEY,
      1
    );

    if (storedVersion < DataMigration.CURRENT_VERSION) {
      await DataMigration.migrate(storedVersion);
      await StorageUtil.setNumber(
        DataMigration.VERSION_KEY,
        DataMigration.CURRENT_VERSION
      );
    }
  }

  /**
   * 执行迁移
   */
  private static async migrate(fromVersion: number): Promise<void> {
    console.info(`[DataMigration] Migrating from v${fromVersion} to v${DataMigration.CURRENT_VERSION}`);

    // 逐版本迁移
    if (fromVersion < 2) {
      await DataMigration.migrateToV2();
    }

    // 未来版本迁移
    // if (fromVersion < 3) {
    //   await DataMigration.migrateToV3();
    // }
  }

  /**
   * 迁移到 V2:添加 badges 字段
   */
  private static async migrateToV2(): Promise<void> {
    const progress = await StorageUtil.getObject<Record<string, unknown>>(
      StorageKeys.USER_PROGRESS,
      {}
    );

    // 添加新字段
    if (!('badges' in progress)) {
      progress['badges'] = [];
    }

    await StorageUtil.setObject(StorageKeys.USER_PROGRESS, progress);
    console.info('[DataMigration] Migrated to V2: added badges field');
  }
}

在应用启动时执行迁移

typescript 复制代码
// EntryAbility.ets
import { DataMigration } from '../common/DataMigration';

export default class EntryAbility extends UIAbility {
  async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): Promise<void> {
    // 初始化存储
    await StorageUtil.init(this.context);

    // 检查数据迁移
    await DataMigration.checkAndMigrate();
  }
}

7.6 实操:实现用户进度持久化

更新 ProgressService

完善 services/ProgressService.ets

typescript 复制代码
/**
 * 进度管理服务
 */
import { StorageUtil } from '../common/StorageUtil';
import { StorageKeys } from '../common/Constants';
import { UserProgress, Badge, DEFAULT_USER_PROGRESS, LearningModule } from '../models/Models';

export class ProgressService {
  private static cachedProgress: UserProgress | null = null;

  /**
   * 加载用户进度
   */
  static async loadProgress(): Promise<UserProgress> {
    try {
      // 优先使用缓存
      if (ProgressService.cachedProgress) {
        return ProgressService.cachedProgress;
      }

      const progress = await StorageUtil.getObject<UserProgress>(
        StorageKeys.USER_PROGRESS,
        DEFAULT_USER_PROGRESS
      );

      // 数据校验和修复
      ProgressService.cachedProgress = ProgressService.validateProgress(progress);
      return ProgressService.cachedProgress;
    } catch (error) {
      console.error('[ProgressService] Load failed:', error);
      return DEFAULT_USER_PROGRESS;
    }
  }

  /**
   * 保存用户进度
   */
  static async saveProgress(progress: UserProgress): Promise<boolean> {
    try {
      const success = await StorageUtil.setObject(StorageKeys.USER_PROGRESS, progress);
      if (success) {
        ProgressService.cachedProgress = progress;
      }
      return success;
    } catch (error) {
      console.error('[ProgressService] Save failed:', error);
      return false;
    }
  }

  /**
   * 验证并修复进度数据
   */
  private static validateProgress(progress: UserProgress): UserProgress {
    return {
      completedLessons: Array.isArray(progress.completedLessons)
        ? progress.completedLessons
        : [],
      completedModules: Array.isArray(progress.completedModules)
        ? progress.completedModules
        : [],
      currentLesson: progress.currentLesson ?? null,
      learningStreak: typeof progress.learningStreak === 'number'
        ? progress.learningStreak
        : 0,
      lastStudyDate: progress.lastStudyDate ?? '',
      totalStudyTime: typeof progress.totalStudyTime === 'number'
        ? progress.totalStudyTime
        : 0,
      badges: Array.isArray(progress.badges) ? progress.badges : []
    };
  }

  /**
   * 标记课程完成
   */
  static async markLessonComplete(lessonId: string, moduleId: string): Promise<void> {
    const progress = await ProgressService.loadProgress();

    if (!progress.completedLessons.includes(lessonId)) {
      progress.completedLessons.push(lessonId);
    }

    progress.currentLesson = lessonId;
    progress.totalStudyTime += 1;

    await ProgressService.saveProgress(progress);
    await ProgressService.updateStreak();
  }

  /**
   * 更新连续学习天数
   */
  static async updateStreak(): Promise<number> {
    const progress = await ProgressService.loadProgress();
    const today = new Date().toISOString().split('T')[0];
    const lastDate = progress.lastStudyDate;

    if (!lastDate) {
      progress.learningStreak = 1;
    } else if (lastDate === today) {
      return progress.learningStreak;
    } else {
      const lastDateObj = new Date(lastDate);
      const todayObj = new Date(today);
      const diffDays = Math.floor(
        (todayObj.getTime() - lastDateObj.getTime()) / (1000 * 60 * 60 * 24)
      );

      progress.learningStreak = diffDays === 1
        ? progress.learningStreak + 1
        : 1;
    }

    progress.lastStudyDate = today;
    await ProgressService.saveProgress(progress);
    return progress.learningStreak;
  }

  /**
   * 计算模块完成百分比
   */
  static getCompletionPercentage(module: LearningModule, progress: UserProgress): number {
    if (module.lessons.length === 0) return 0;

    const completedCount = module.lessons.filter(
      lesson => progress.completedLessons.includes(lesson.id)
    ).length;

    return Math.round((completedCount / module.lessons.length) * 100);
  }

  /**
   * 添加徽章
   */
  static async addBadge(badge: Badge): Promise<void> {
    const progress = await ProgressService.loadProgress();

    if (!progress.badges.some(b => b.id === badge.id)) {
      progress.badges.push(badge);
      await ProgressService.saveProgress(progress);
    }
  }

  /**
   * 重置进度
   */
  static async resetProgress(): Promise<void> {
    await ProgressService.saveProgress(DEFAULT_USER_PROGRESS);
    ProgressService.cachedProgress = null;
  }

  /**
   * 清除缓存
   */
  static clearCache(): void {
    ProgressService.cachedProgress = null;
  }
}

在页面中使用

typescript 复制代码
@Entry
@Component
struct Index {
  @State progress: UserProgress = DEFAULT_USER_PROGRESS;
  @State isLoading: boolean = true;

  async aboutToAppear(): Promise<void> {
    // 初始化存储
    await StorageUtil.init(getContext(this));

    // 加载进度
    this.progress = await ProgressService.loadProgress();
    this.isLoading = false;
  }

  build() {
    Column() {
      if (this.isLoading) {
        LoadingProgress()
      } else {
        Text(`已完成 ${this.progress.completedLessons.length} 课`)
        Text(`连续学习 ${this.progress.learningStreak} 天`)
      }
    }
  }
}

本次课程小结

通过本次课程,你已经:

✅ 了解了 HarmonyOS 数据存储方案

✅ 掌握了 Preferences 存储的使用

✅ 完善了 StorageUtil 工具类

✅ 学会了异步存储操作和错误处理

✅ 了解了数据迁移与版本管理

✅ 实现了用户进度的持久化存储


课后练习

  1. 实现收藏持久化:创建 BookmarkService,实现收藏的增删查

  2. 添加数据导出:实现将用户数据导出为 JSON 文件

  3. 实现数据同步:当网络可用时,将本地数据同步到云端


下次预告

第8次:主题系统实现

我们将完善应用的主题系统:

  • 主题模式设计(AUTO/LIGHT/DARK)
  • 主题颜色配置
  • 系统颜色模式检测
  • 主题切换与持久化

打造专业的视觉体验!

相关推荐
枫叶丹41 小时前
【HarmonyOS 6.0】Camera Kit 微距状态监听能力详解
开发语言·华为·harmonyos
科技前沿资讯1 小时前
聚焦AWE 2026:华为鸿蒙智家,定义全场景智慧生活新范式
华为·生活·harmonyos
Highcharts.js11 小时前
Highcharts React v4.2.1 正式发布:更自然的React开发体验,更清晰的数据处理
linux·运维·javascript·ubuntu·react.js·数据可视化·highcharts
sxq15 小时前
Flow Render: 像调用异步函数一样渲染 UI 组件
vue.js·react.js
Sgf22716 小时前
如何阅读 React 源码:系统化学习指南
前端·react.js·前端框架
sure28218 小时前
React Native中自定义TabBar
前端·react native·react.js
lauo19 小时前
dtnsbot分身网页版正式上线:开启“灵魂与肉身分离”的智能体远程控制新纪元
人工智能·智能手机·架构·开源·github
局i19 小时前
React 简单地图组件封装:基于高德地图 API 的实践(附源码)
前端·javascript·react.js