第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 工具类
✅ 学会了异步存储操作和错误处理
✅ 了解了数据迁移与版本管理
✅ 实现了用户进度的持久化存储
课后练习
-
实现收藏持久化:创建 BookmarkService,实现收藏的增删查
-
添加数据导出:实现将用户数据导出为 JSON 文件
-
实现数据同步:当网络可用时,将本地数据同步到云端
下次预告
第8次:主题系统实现
我们将完善应用的主题系统:
- 主题模式设计(AUTO/LIGHT/DARK)
- 主题颜色配置
- 系统颜色模式检测
- 主题切换与持久化
打造专业的视觉体验!