及时做APP开发实战(二)-数据持久化实战
及时做APP开发实战(二)-数据持久化实战数据持久化是移动应用的核心能力之一。本文以及时做应用为例详细介绍如何在HarmonyOS NEXT中使用Preferences API实现高效的数据存储方案。一、数据持久化方案选型HarmonyOS NEXT提供了多种数据存储方案每种都有其适用场景方案适用场景特点Preferences配置信息、小型数据轻量级、键值对存储RdbStore结构化数据、复杂查询关系型数据库DistributedData分布式数据同步多设备协同【图1HarmonyOS数据存储方案对比图】对于及时做这样的待办应用数据量相对较小且不需要复杂查询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 基本使用流程importdataPreferencesfromohos.data.preferences;importcommonfromohos.app.ability.common;// 1. 获取Preferences实例constcontextgetContext(this)ascommon.UIAbilityContext;constpreferencesawaitdataPreferences.getPreferences(context,my_preferences);// 2. 写入数据awaitpreferences.put(key,value);awaitpreferences.flush();// 持久化到文件// 3. 读取数据constvalueawaitpreferences.get(key,default_value);三、封装PreferencesHelper直接使用Preferences API存在以下问题每次都需要获取context重复的异步处理代码缺少统一的错误处理我们封装一个PreferencesHelper类来解决这些问题。3.1 单例模式设计exportclassPreferencesHelper{privatestaticINSTANCE:PreferencesHelper|nullnull;privatepreferences:dataPreferences.Preferences|nullnull;privateconstructor(){}/** * 获取单例实例 */staticasyncgetInstance():PromisePreferencesHelper{if(PreferencesHelper.INSTANCEnull){PreferencesHelper.INSTANCEnewPreferencesHelper();awaitPreferencesHelper.INSTANCE.init();}returnPreferencesHelper.INSTANCE;}/** * 初始化Preferences */privateasyncinit():Promisevoid{try{constcontextgetContext(this)ascommon.UIAbilityContext;this.preferencesawaitdataPreferences.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_KEYtodo_list;/** * 获取所有待办任务 */asyncgetTodoList():PromiseTodoItem[]{if(!this.preferences){return[];}try{constjsonStrawaitthis.preferences.get(PreferencesHelper.TODO_LIST_KEY,[])asstring;returnJSON.parse(jsonStr)asTodoItem[];}catch(error){console.error(getTodoList failed:,error);return[];}}/** * 保存待办任务列表 */asyncsaveTodoList(todoList:TodoItem[]):Promisevoid{if(!this.preferences){return;}try{constjsonStrJSON.stringify(todoList);awaitthis.preferences.put(PreferencesHelper.TODO_LIST_KEY,jsonStr);awaitthis.preferences.flush();}catch(error){console.error(saveTodoList failed:,error);}}关键点JSON序列化将对象数组转为字符串存储默认值读取时提供默认值’[]避免nullflush调用put后必须flush才能持久化类型转换使用as string和as TodoItem[]确保类型安全3.3 CRUD操作封装基于getTodoList和saveTodoList我们可以实现完整的CRUD/** * 添加待办任务 */asyncaddTodo(todo:TodoItem):Promisevoid{consttodoListawaitthis.getTodoList();todoList.push(todo);awaitthis.saveTodoList(todoList);}/** * 更新待办任务 */asyncupdateTodo(todo:TodoItem):Promisevoid{consttodoListawaitthis.getTodoList();constindextodoList.findIndex(itemitem.idtodo.id);if(index!-1){todoList[index]todo;awaitthis.saveTodoList(todoList);}}/** * 删除待办任务 */asyncdeleteTodo(todoId:number):Promisevoid{consttodoListawaitthis.getTodoList();constfilteredListtodoList.filter(itemitem.id!todoId);awaitthis.saveTodoList(filteredList);}这种设计模式称为读-改-写模式先读取全部数据在内存中修改再整体写回虽然看起来效率不高但对于小型数据几十条待办完全够用且代码简洁。四、番茄钟记录存储4.1 独立存储设计番茄钟记录与待办任务是两类独立数据我们使用不同的key存储privatestaticreadonlyPOMODORO_RECORDS_KEYpomodoro_records;/** * 添加番茄钟记录 */asyncaddPomodoroRecord(record:PomodoroRecord):Promisevoid{constrecordsawaitthis.getPomodoroRecords();records.push(record);awaitthis.savePomodoroRecords(records);}/** * 获取指定待办的番茄钟记录 */asyncgetPomodoroRecordsByTodoId(todoId:number):PromisePomodoroRecord[]{constrecordsawaitthis.getPomodoroRecords();returnrecords.filter(recordrecord.todoIdtodoId);}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 │└─────────────────────────────────────────────────────────────────────┘【图3Preferences存储结构示意图】五、性能优化策略5.1 批量操作避免频繁的读写操作可以批量处理// 不推荐每次操作都读写for(consttodoofnewTodos){awaitpreferencesHelper.addTodo(todo);}// 推荐一次性添加constexistingListawaitpreferencesHelper.getTodoList();existingList.push(...newTodos);awaitpreferencesHelper.saveTodoList(existingList);5.2 内存缓存在ViewModel中维护内存缓存减少读取次数exportclassTodoViewModel{privatetodoList:TodoItem[][];// 内存缓存asyncinit():Promisevoid{this.todoListawaitthis.preferencesHelper.getTodoList();}getTodoList():TodoItem[]{returnthis.todoList;// 直接返回缓存}asyncaddTodo(text:string):PromiseTodoItem{consttodocreateTodoItem(text);this.todoList.push(todo);// 更新缓存awaitthis.preferencesHelper.addTodo(todo);// 异步持久化returntodo;}}六、错误处理与容错6.1 初始化失败处理privateasyncinit():Promisevoid{try{constcontextgetContext(this)ascommon.UIAbilityContext;this.preferencesawaitdataPreferences.getPreferences(context,PreferencesHelper.PREFERENCES_NAME);}catch(error){console.error(PreferencesHelper init failed:,error);// 可以在这里添加重试逻辑或降级方案}}6.2 数据损坏处理asyncgetTodoList():PromiseTodoItem[]{if(!this.preferences){return[];// 返回空数组而不是null}try{constjsonStrawaitthis.preferences.get(PreferencesHelper.TODO_LIST_KEY,[])asstring;returnJSON.parse(jsonStr)asTodoItem[];}catch(error){console.error(getTodoList failed:,error);return[];// 解析失败返回空数组}}关键原则永不返回null返回空数组或默认值记录错误日志便于问题排查降级处理存储失败时至少保证应用不崩溃七、完整代码示例importdataPreferencesfromohos.data.preferences;importcommonfromohos.app.ability.common;import{TodoItem}from../model/TodoItem;import{PomodoroRecord}from../model/PomodoroRecord;exportclassPreferencesHelper{privatestaticINSTANCE:PreferencesHelper|nullnull;privatepreferences:dataPreferences.Preferences|nullnull;privatestaticreadonlyTODO_LIST_KEYtodo_list;privatestaticreadonlyPOMODORO_RECORDS_KEYpomodoro_records;privatestaticreadonlyPREFERENCES_NAMEtimelydo_preferences;privateconstructor(){}staticasyncgetInstance():PromisePreferencesHelper{if(PreferencesHelper.INSTANCEnull){PreferencesHelper.INSTANCEnewPreferencesHelper();awaitPreferencesHelper.INSTANCE.init();}returnPreferencesHelper.INSTANCE;}privateasyncinit():Promisevoid{try{constcontextgetContext(this)ascommon.UIAbilityContext;this.preferencesawaitdataPreferences.getPreferences(context,PreferencesHelper.PREFERENCES_NAME);}catch(error){console.error(PreferencesHelper init failed:,error);}}// ... 其他CRUD方法}八、最佳实践总结单例模式全局唯一实例避免重复初始化异步处理统一使用async/await避免回调地狱类型安全使用TypeScript类型注解减少运行时错误错误处理关键操作添加try-catch保证应用稳定性数据序列化复杂对象使用JSON序列化存储内存缓存ViewModel层维护缓存减少IO操作批量操作避免频繁读写提升性能通过这套封装方案及时做应用实现了高效、可靠的数据持久化能力为后续功能开发提供了坚实的数据基础。