及时做APP开发实战(二)-数据持久化实战
数据持久化是移动应用的核心能力之一。本文以"及时做"应用为例,详细介绍如何在HarmonyOS NEXT中使用Preferences API实现高效的数据存储方案。
一、数据持久化方案选型
HarmonyOS NEXT提供了多种数据存储方案,每种都有其适用场景:
| 方案 | 适用场景 | 特点 |
|---|---|---|
| Preferences | 配置信息、小型数据 | 轻量级、键值对存储 |
| RdbStore | 结构化数据、复杂查询 | 关系型数据库 |
| DistributedData | 分布式数据同步 | 多设备协同 |
【图1:HarmonyOS数据存储方案对比图】
对于"及时做"这样的待办应用,数据量相对较小,且不需要复杂查询,Preferences是最佳选择。
二、Preferences API基础
2.1 工作原理
Preferences采用键值对(Key-Value)方式存储数据,类似于Android的SharedPreferences或iOS的UserDefaults。
┌─────────────────────────────────┐ │ 应用进程 │ │ ┌───────────────────────────┐ │ │ │ Preferences实例 │ │ │ │ - 内存缓存 │ │ │ │ - put/get操作 │ │ │ │ - flush持久化 │ │ │ └───────────────────────────┘ │ └─────────────┬───────────────────┘ │ 文件IO ┌─────────────▼───────────────────┐ │ 本地XML文件存储 │ │ /data/app/el2/100/base/... │ └─────────────────────────────────┘2.2 基本使用流程
importdataPreferencesfrom'@ohos.data.preferences';importcommonfrom'@ohos.app.ability.common';// 1. 获取Preferences实例constcontext=getContext(this)ascommon.UIAbilityContext;constpreferences=awaitdataPreferences.getPreferences(context,'my_preferences');// 2. 写入数据awaitpreferences.put('key','value');awaitpreferences.flush();// 持久化到文件// 3. 读取数据constvalue=awaitpreferences.get('key','default_value');三、封装PreferencesHelper
直接使用Preferences API存在以下问题:
- 每次都需要获取context
- 重复的异步处理代码
- 缺少统一的错误处理
我们封装一个PreferencesHelper类来解决这些问题。
3.1 单例模式设计
exportclassPreferencesHelper{privatestaticINSTANCE:PreferencesHelper|null=null;privatepreferences:dataPreferences.Preferences|null=null;privateconstructor(){}/** * 获取单例实例 */staticasyncgetInstance():Promise<PreferencesHelper>{if(PreferencesHelper.INSTANCE===null){PreferencesHelper.INSTANCE=newPreferencesHelper();awaitPreferencesHelper.INSTANCE.init();}returnPreferencesHelper.INSTANCE;}/** * 初始化Preferences */privateasyncinit():Promise<void>{try{constcontext=getContext(this)ascommon.UIAbilityContext;this.preferences=awaitdataPreferences.getPreferences(context,PreferencesHelper.PREFERENCES_NAME);}catch(error){console.error('PreferencesHelper init failed:',error);}}}┌─────────────────────────────────────────────────────────────┐
│ getInstance() 调用流程 │
└─────────────────────────────────────────────────────────────┘
调用 getInstance() │ ▼ ┌─────────────────────┐ │ INSTANCE === null ? │ └──────────┬──────────┘ │ ┌───────┴───────┐ │ │ ▼ ▼ [是] [否] │ │ ▼ │┌──────────────┐ │
│ new实例 │ │
│ INSTANCE = │ │
│ new Helper() │ │
└──────┬───────┘ │
│ │
▼ │
┌──────────────┐ │
│ init() │ │
│ 初始化 │ │
│ Preferences │ │
└──────┬───────┘ │
│ │
└───────┬───────┘
│
▼
返回 INSTANCE
│
▼
┌─────────────────────┐
│ 全局唯一实例 │
└─────────────────────┘
【图2:单例模式获取流程图】
设计要点:
- 私有构造函数:防止外部new实例
- 静态INSTANCE:全局唯一实例
- 延迟初始化:首次使用时才初始化
- 错误处理:init失败时记录日志
3.2 待办数据存储实现
对于复杂对象,我们需要序列化为JSON字符串存储:
// 存储键名privatestaticreadonlyTODO_LIST_KEY='todo_list';/** * 获取所有待办任务 */asyncgetTodoList():Promise<TodoItem[]>{if(!this.preferences){return[];}try{constjsonStr=awaitthis.preferences.get(PreferencesHelper.TODO_LIST_KEY,'[]')asstring;returnJSON.parse(jsonStr)asTodoItem[];}catch(error){console.error('getTodoList failed:',error);return[];}}/** * 保存待办任务列表 */asyncsaveTodoList(todoList:TodoItem[]):Promise<void>{if(!this.preferences){return;}try{constjsonStr=JSON.stringify(todoList);awaitthis.preferences.put(PreferencesHelper.TODO_LIST_KEY,jsonStr);awaitthis.preferences.flush();}catch(error){console.error('saveTodoList failed:',error);}}关键点:
- JSON序列化:将对象数组转为字符串存储
- 默认值:读取时提供默认值’[]'避免null
- flush调用:put后必须flush才能持久化
- 类型转换:使用as string和as TodoItem[]确保类型安全
3.3 CRUD操作封装
基于getTodoList和saveTodoList,我们可以实现完整的CRUD:
/** * 添加待办任务 */asyncaddTodo(todo:TodoItem):Promise<void>{consttodoList=awaitthis.getTodoList();todoList.push(todo);awaitthis.saveTodoList(todoList);}/** * 更新待办任务 */asyncupdateTodo(todo:TodoItem):Promise<void>{consttodoList=awaitthis.getTodoList();constindex=todoList.findIndex(item=>item.id===todo.id);if(index!==-1){todoList[index]=todo;awaitthis.saveTodoList(todoList);}}/** * 删除待办任务 */asyncdeleteTodo(todoId:number):Promise<void>{consttodoList=awaitthis.getTodoList();constfilteredList=todoList.filter(item=>item.id!==todoId);awaitthis.saveTodoList(filteredList);}这种设计模式称为"读-改-写"模式:
- 先读取全部数据
- 在内存中修改
- 再整体写回
虽然看起来效率不高,但对于小型数据(几十条待办)完全够用,且代码简洁。
四、番茄钟记录存储
4.1 独立存储设计
番茄钟记录与待办任务是两类独立数据,我们使用不同的key存储:
privatestaticreadonlyPOMODORO_RECORDS_KEY='pomodoro_records';/** * 添加番茄钟记录 */asyncaddPomodoroRecord(record:PomodoroRecord):Promise<void>{constrecords=awaitthis.getPomodoroRecords();records.push(record);awaitthis.savePomodoroRecords(records);}/** * 获取指定待办的番茄钟记录 */asyncgetPomodoroRecordsByTodoId(todoId:number):Promise<PomodoroRecord[]>{constrecords=awaitthis.getPomodoroRecords();returnrecords.filter(record=>record.todoId===todoId);}4.2 数据结构示意
存储在Preferences中的数据结构:
{"todo_list":"[{\"id\":123,\"text\":\"学习ArkTS\",\"completed\":false,...}]","pomodoro_records":"[{\"id\":456,\"todoId\":123,\"duration\":25,...}]"}┌────────────────────────────────────────────────────────────────────┐
│ Preferences 存储架构 │
└────────────────────────────────────────────────────────────────────┘
应用层 ┌──────────────────┐ │ TodoViewModel │ │ ┌────────────┐ │ │ │ todoList[] │ │ ← 内存缓存 │ └────────────┘ │ └────────┬─────────┘ │ ▼ ┌──────────────────┐ │ PreferencesHelper│ │ ┌────────────┐ │ │ │preferences │ │ ← Preferences实例 │ └────────────┘ │ └────────┬─────────┘ │ │ put() / flush() ▼┌─────────────────────────────────────────────────────────────────────┐
│ Preferences 存储 │
├──────────────────┬──────────────────────────────────────────────────┤
│ Key │ Value (JSON String) │
├──────────────────┼──────────────────────────────────────────────────┤
│ “todo_list” │ [{“id”:1,“text”:“学习ArkTS”,“completed”:false}] │
├──────────────────┼──────────────────────────────────────────────────┤
│ "pomodoro_records"│ [{“id”:101,“todoId”:1,“duration”:25}] │
├──────────────────┼──────────────────────────────────────────────────┤
│ “settings” │ {“pomodoroDuration”:25,“soundEnabled”:true} │
└──────────────────┴──────────────────────────────────────────────────┘
│
│ 文件IO
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 物理文件存储 │
│ /data/app/el2/100/base/com.example.timelydo/ │
│ preferences/timelydo_preferences.xml │
└─────────────────────────────────────────────────────────────────────┘
【图3:Preferences存储结构示意图】
五、性能优化策略
5.1 批量操作
避免频繁的读写操作,可以批量处理:
// 不推荐:每次操作都读写for(consttodoofnewTodos){awaitpreferencesHelper.addTodo(todo);}// 推荐:一次性添加constexistingList=awaitpreferencesHelper.getTodoList();existingList.push(...newTodos);awaitpreferencesHelper.saveTodoList(existingList);5.2 内存缓存
在ViewModel中维护内存缓存,减少读取次数:
exportclassTodoViewModel{privatetodoList:TodoItem[]=[];// 内存缓存asyncinit():Promise<void>{this.todoList=awaitthis.preferencesHelper.getTodoList();}getTodoList():TodoItem[]{returnthis.todoList;// 直接返回缓存}asyncaddTodo(text:string):Promise<TodoItem>{consttodo=createTodoItem(text);this.todoList.push(todo);// 更新缓存awaitthis.preferencesHelper.addTodo(todo);// 异步持久化returntodo;}}六、错误处理与容错
6.1 初始化失败处理
privateasyncinit():Promise<void>{try{constcontext=getContext(this)ascommon.UIAbilityContext;this.preferences=awaitdataPreferences.getPreferences(context,PreferencesHelper.PREFERENCES_NAME);}catch(error){console.error('PreferencesHelper init failed:',error);// 可以在这里添加重试逻辑或降级方案}}6.2 数据损坏处理
asyncgetTodoList():Promise<TodoItem[]>{if(!this.preferences){return[];// 返回空数组而不是null}try{constjsonStr=awaitthis.preferences.get(PreferencesHelper.TODO_LIST_KEY,'[]')asstring;returnJSON.parse(jsonStr)asTodoItem[];}catch(error){console.error('getTodoList failed:',error);return[];// 解析失败返回空数组}}关键原则:
- 永不返回null:返回空数组或默认值
- 记录错误日志:便于问题排查
- 降级处理:存储失败时至少保证应用不崩溃
七、完整代码示例
importdataPreferencesfrom'@ohos.data.preferences';importcommonfrom'@ohos.app.ability.common';import{TodoItem}from'../model/TodoItem';import{PomodoroRecord}from'../model/PomodoroRecord';exportclassPreferencesHelper{privatestaticINSTANCE:PreferencesHelper|null=null;privatepreferences:dataPreferences.Preferences|null=null;privatestaticreadonlyTODO_LIST_KEY='todo_list';privatestaticreadonlyPOMODORO_RECORDS_KEY='pomodoro_records';privatestaticreadonlyPREFERENCES_NAME='timelydo_preferences';privateconstructor(){}staticasyncgetInstance():Promise<PreferencesHelper>{if(PreferencesHelper.INSTANCE===null){PreferencesHelper.INSTANCE=newPreferencesHelper();awaitPreferencesHelper.INSTANCE.init();}returnPreferencesHelper.INSTANCE;}privateasyncinit():Promise<void>{try{constcontext=getContext(this)ascommon.UIAbilityContext;this.preferences=awaitdataPreferences.getPreferences(context,PreferencesHelper.PREFERENCES_NAME);}catch(error){console.error('PreferencesHelper init failed:',error);}}// ... 其他CRUD方法}八、最佳实践总结
- 单例模式:全局唯一实例,避免重复初始化
- 异步处理:统一使用async/await,避免回调地狱
- 类型安全:使用TypeScript类型注解,减少运行时错误
- 错误处理:关键操作添加try-catch,保证应用稳定性
- 数据序列化:复杂对象使用JSON序列化存储
- 内存缓存:ViewModel层维护缓存,减少IO操作
- 批量操作:避免频繁读写,提升性能
通过这套封装方案,"及时做"应用实现了高效、可靠的数据持久化能力,为后续功能开发提供了坚实的数据基础。