HarmonyOs应用《重要日》开发第5篇 - 架构模式:MainViewModel 详解

📅 2026/7/16 5:05:49
HarmonyOs应用《重要日》开发第5篇 - 架构模式:MainViewModel 详解
本篇深入剖析 ImportantDays 项目的核心架构组件——MainViewModel。作为全局唯一的单例 ViewModel它统一管理所有重要日数据、状态和业务逻辑是连接 UI 与数据的桥梁。一、为什么选择单例 ViewModel1.1 项目特点分析ImportantDays 是一款单模块应用三个主页面重要日列表、日历、我的共享同一份重要日数据。如果每个页面各自管理数据会导致数据不一致列表页添加了重要日日历页看不到代码重复每个页面都要写一套 CRUD 逻辑状态同步困难排序方式、选中项等状态需要跨页面共享1.2 单例模式的优势采用单例 ViewModel 后唯一数据源所有页面共享同一个dayList数据始终一致统一逻辑CRUD、排序、统计等逻辑集中在一处状态共享排序方式、当前 Tab、选中 ID 全局可访问简化通信页面间无需通过 Intent 或事件总线传递数据二、MainViewModel 实现2.1 类定义与单例// viewmodel/MainViewModel.etsObservedV2exportclassMainViewModel{privatestatic_instance:MainViewModel|nullnull;privateprefUtil:PreferenceUtil|nullnull;TracedayList:ImportantDay[][];TracesortType:numberSortType.BY_DATE;TracecurTabIndex:number0;TraceselectedDayId:string;privateconstructor(){}publicstaticgetinstance():MainViewModel{if(!MainViewModel._instance){MainViewModel._instancenewMainViewModel();}returnMainViewModel._instance;}}2.2 单例模式要点私有构造函数private constructor()防止外部直接new静态实例private static _instance持有唯一实例静态访问器get instance()提供全局访问点懒加载首次访问时才创建实例2.3 ObservedV2 TraceObservedV2exportclassMainViewModel{TracedayList:ImportantDay[][];// 重要日列表TracesortType:numberSortType.BY_DATE;// 排序方式TracecurTabIndex:number0;// 当前 Tab 索引TraceselectedDayId:string;// 当前选中的重要日 ID}这四个Trace属性都是全局响应式的任何页面通过Monitor监听这些属性的变化都能在数据更新时自动刷新 UI。三、初始化与数据加载3.1 初始化方法publicasyncinit(context:Context):Promisevoid{this.prefUtilPreferenceUtil.getInstance(context,AppConstants.PREF_FILE);this.loadDays();Logger.info(MainViewModel: init, loaded${this.dayList.length}days);}在EntryAbility.onCreate中调用// entryability/EntryAbility.etsonCreate(want:Want,launchParam:AbilityConstant.LaunchParam):void{this.context.getApplicationContext().setColorMode(ConfigurationConstant.ColorMode.COLOR_MODE_NOT_SET);this.vm.init(this.context);}3.2 加载数据privateloadDays():void{if(!this.prefUtil){return;}constjsonStrthis.prefUtil.get(AppConstants.PREF_KEY_DAYS,[])asstring;try{constarr:string[]JSON.parse(jsonStr);this.dayListarr.map((json:string)ImportantDay.fromJSON(json)).filter((day:ImportantDay|null)day!null)asImportantDay[];}catch(e){Logger.error(MainViewModel: loadDays error:${JSON.stringify(e)});this.dayList[];}}加载流程从 Preferences 读取 JSON 字符串解析为字符串数组每个元素是一个 ImportantDay 的 JSON逐个反序列化为 ImportantDay 对象过滤掉反序列化失败的结果null赋值给dayList触发 UI 更新3.3 保存数据privatesaveDays():void{if(!this.prefUtil){return;}constarr:string[]this.dayList.map((day:ImportantDay)day.toJSON());this.prefUtil.put(AppConstants.PREF_KEY_DAYS,JSON.stringify(arr));}存储结构Preferences[important_day_list] [{json1}, {json2}, {json3}]外层是 JSON 数组字符串每个元素是 ImportantDay 的 JSON 字符串。这种嵌套 JSON 设计是为了避免 Preferences 对数组类型的直接处理问题。四、CRUD 操作4.1 添加CreatepublicaddDay(day:ImportantDay):void{this.dayList.push(day);this.saveDays();}简单直接push 到数组末尾然后持久化。由于dayList是Trace装饰的push 操作会触发 UI 更新。4.2 更新UpdatepublicupdateDay(day:ImportantDay):void{constindexthis.dayList.findIndex((d:ImportantDay)d.idday.id);if(index!-1){day.updatedAtDate.now();this.dayList[index]day;this.saveDays();}}通过 ID 查找索引替换对象。注意更新updatedAt时间戳。4.3 删除DeletepublicdeleteDay(id:string):void{this.dayListthis.dayList.filter((d:ImportantDay)d.id!id);this.saveDays();}使用filter创建新数组这比splice更安全在 ArkUI V2 中能确保触发响应式更新。4.4 查询ReadpublicgetDayById(id:string):ImportantDay|undefined{returnthis.dayList.find((d:ImportantDay)d.idid);}4.5 置顶切换publictogglePin(id:string):void{constdaythis.dayList.find((d:ImportantDay)d.idid);if(day){day.isPinned!day.isPinned;day.updatedAtDate.now();this.saveDays();}}直接修改对象的isPinned属性由于ImportantDay的isPinned是Trace装饰的UI 会自动更新。五、排序逻辑5.1 排序实现publicgetSortedDays():ImportantDay[]{constsorted[...this.dayList];sorted.sort((a:ImportantDay,b:ImportantDay){// 置顶优先if(a.isPinned!b.isPinned)return-1;if(!a.isPinnedb.isPinned)return1;// 然后按排序类型if(this.sortTypeSortType.BY_DATE){returna.date.localeCompare(b.date);}elseif(this.sortTypeSortType.BY_CREATED){returnb.createdAt-a.createdAt;}elseif(this.sortTypeSortType.BY_COUNTDOWN){constaDaysthis.getEffectiveDays(a);constbDaysthis.getEffectiveDays(b);returnaDays-bDays;}return0;});returnsorted;}5.2 排序规则排序类型规则按日期日期从早到晚按创建时间最新创建的在前按倒计时距离最近的重要日在前无论哪种排序置顶的重要日始终在最前面。5.3 使用展开运算符避免副作用constsorted[...this.dayList];// 创建副本sorted.sort(...);// 对副本排序不直接对this.dayList排序避免修改原始数组的顺序。六、重复事件处理6.1 获取有效日期publicgetEffectiveDate(day:ImportantDay):string{if(day.repeatType!RepeatType.NONE){returnDateUtil.getNextOccurrence(day.date,day.repeatType);}returnday.date;}对于重复事件有效日期是下一次发生的日期而非原始日期。6.2 获取有效天数publicgetEffectiveDays(day:ImportantDay):number{leteffectiveDateday.date;if(day.repeatType!RepeatType.NONE){effectiveDateDateUtil.getNextOccurrence(day.date,day.repeatType);}returnDateUtil.daysFromToday(effectiveDate);}6.3 获取计数文本publicgetCountText(day:ImportantDay):CountText{consteffectiveDatethis.getEffectiveDate(day);constdaysDateUtil.daysFromToday(effectiveDate);if(days0){return{value:0,suffix:今天,isToday:true};}if(day.countModeCountMode.COUNTDOWN){return{value:Math.abs(days),suffix:days0?天后:天前,isToday:false};}else{constpassedDays-days;return{value:Math.abs(passedDays),suffix:天,isToday:false};}}计数逻辑倒数模式未来日期显示X天后过去日期显示X天前正数模式始终显示X天表示已过去的天数今天显示今天和 图标七、统计逻辑publicgetStats():Stats{letupcoming0;letpassed0;letthisMonth0;for(constdayofthis.dayList){consteffectiveDatethis.getEffectiveDate(day);constdaysDateUtil.daysFromToday(effectiveDate);if(days0)upcoming;elseif(days0)passed;if(DateUtil.isThisMonth(effectiveDate))thisMonth;}return{total:this.dayList.length,upcoming,passed,thisMonth,};}统计逻辑考虑了重复事件——使用有效日期而非原始日期来计算。八、日历查询8.1 查询某日的重要日publicgetDaysForDate(date:string):ImportantDay[]{returnthis.dayList.filter((day:ImportantDay){if(day.repeatTypeRepeatType.YEARLY){constdayMonthDateUtil.formatDate(day.date,MM-DD);consttargetMonthDateUtil.formatDate(date,MM-DD);returndayMonthtargetMonth;}elseif(day.repeatTypeRepeatType.MONTHLY){constdayDayDateUtil.formatDate(day.date,DD);consttargetDayDateUtil.formatDate(date,DD);returndayDaytargetDay;}returnday.datedate;});}每年重复只比较月和日MM-DD每月重复只比较日DD不重复完整日期匹配8.2 查询某月的重要日publicgetDaysForMonth(year:number,month:number):Mapstring,ImportantDay[]{constmapnewMapstring,ImportantDay[]();for(constdayofthis.dayList){letmatchDate:string|nullnull;if(day.repeatTypeRepeatType.YEARLY){constdayDateDateUtil.getYearMonthFromDate(day.date);if(dayDate.monthmonth){constdayOfMonthparseInt(DateUtil.formatDate(day.date,DD));constdaysInMonthDateUtil.getDaysInMonth(year,month);if(dayOfMonthdaysInMonth){matchDate${year}-${String(month).padStart(2,0)}-${String(dayOfMonth).padStart(2,0)};}}}elseif(day.repeatTypeRepeatType.MONTHLY){constdayOfMonthparseInt(DateUtil.formatDate(day.date,DD));constdaysInMonthDateUtil.getDaysInMonth(year,month);if(dayOfMonthdaysInMonth){matchDate${year}-${String(month).padStart(2,0)}-${String(dayOfMonth).padStart(2,0)};}}else{constdayYMDateUtil.getYearMonthFromDate(day.date);if(dayYM.yearyeardayYM.monthmonth){matchDateday.date;}}if(matchDate){if(!map.has(matchDate)){map.set(matchDate,[]);}map.get(matchDate)!.push(day);}}returnmap;}这个方法返回一个Map日期, ImportantDay[]用于日历视图在某月内标记有重要日的日期。特别处理了2月29日这种在非闰年不存在的情况。九、ViewModel 在 UI 中的使用9.1 页面中获取实例// 任何页面或组件中privatevm:MainViewModelMainViewModel.instance;9.2 监听数据变化ComponentV2exportstruct ImportantDayListPage{privatevm:MainViewModelMainViewModel.instance;Monitor(vm.dayList)onDayListChange():void{this.refreshList();}}9.3 调用业务方法// 添加this.vm.addDay(day);// 删除this.vm.deleteDay(id);// 置顶this.vm.togglePin(id);// 获取排序后的列表constsortedthis.vm.getSortedDays();// 获取统计conststatsthis.vm.getStats();十、小结MainViewModel 作为单例 ViewModel集中管理了应用的所有数据状态和业务逻辑。它的设计简洁而完整——CRUD 操作、排序、统计、重复事件处理、日历查询所有核心逻辑都在这里。通过ObservedV2TraceMonitor的配合实现了数据驱动的响应式 UI 更新。