
首页数据聚合——从多个 Manager 到统一的视图模型引言在实际的 HarmonyOS 应用开发中一个页面往往需要从多个数据源获取数据。例如首页可能需要同时展示今日学习进度来自LearningPlanManager、生词本数量来自NewWordManager、错题列表来自PreferenceUtil以及用户信息来自AppStorageV2。如何协调这些数据源、保持 UI 与数据同步、并在页面加载时正确初始化数据是每个开发者都需要面对的挑战。本文将通过HomePage的代码实例分析多数据源聚合的最佳实践。一、多数据源的整体架构首页HomePage的数据来源如下图所示数据来源数据内容获取方式LearningPlanManager今日学习进度、连续天数单例 Manager 的 getterNewWordManager生词本数量单例 Manager 的 getterPreferenceUtil错题记录列表键值直接读取AppStorageV2用户登录信息全局状态连接HomeVM断点响应式信息ViewModel 单例二、UI 状态管理Local 与 Trace在 ArkTS V2 的组件化架构中Local装饰器用于管理组件级别的状态ComponentV2exportstruct HomePage{homeVM:HomeVMHomeVM.instance;privatelistData:PracticeView[]PRACTICE_LIST_DATA;privatedataSource:PracticeDataSourcenewPracticeDataSource(this.listData);Localfunctions:CourseCoreBar[][newCourseCoreBar($r(app.media.ic_practice_camera),搜单词),newCourseCoreBar($r(app.media.ic__practice_wrong_question),易错词),newCourseCoreBar($r(app.media.ic_practice_star),生词本),newCourseCoreBar($r(app.media.ic_practice_notes),笔记),newCourseCoreBar($r(app.media.ic_chart_report),学习报告)];LocalcoursePractice:CourseQuestions[][newCourseQuestions(CET-4 核心词汇,120,300,85,2025-01-15 14:30),newCourseQuestions(CET-6 高频词汇,1024,3411,85,2025-01-15 14:30),];Localprop:SampleAppStorageV2.connect(Sample,()newSample())!;LocallogoUser:UserInfoAppStorageV2.connect(UserInfo,()newUserInfo())!;}状态管理原则Local管理 UI 状态所有可能被 UI 更新的数据都使用Local或Trace装饰确保状态变更时触发 UI 刷新。AppStorageV2.connect全局用户状态通过 AppStorageV2 连接实现跨页面共享和自动同步。普通属性存业务数据listData等纯配置数据作为普通属性存储不参与状态追踪。三、页面初始化数据流程aboutToAppear是页面生命周期中初始化数据的最佳时机aboutToAppear():void{this.propAppStorageV2.connect(Sample,Sample,()newSample())!;// 其他 Manager 数据的初始化在使用时按需获取}首页中的数据获取是懒加载而非预加载的——数据在各个 Builder 方法中按需从 Manager 读取BuildertodayStudyBuilder(){Column(){Row(){Text(今日学习).fontSize(16).fontWeight(FontWeight.Medium);Text(坚持 7 天)// 从 LearningPlanManager 获取连续天数.fontSize(12).fontColor($r(sys.color.font_secondary)).layoutWeight(1).textAlign(TextAlign.End);}.width(100%).margin({bottom:8});Row(){Text(已学 15 词 / 目标 30 词)// 从 LearningPlanManager 获取进度.fontSize(13).fontColor($r(sys.color.font_secondary)).layoutWeight(1);Text(50%).fontSize(20).fontWeight(FontWeight.Bold).fontColor($r(sys.color.comp_background_emphasize));}.width(100%).margin({bottom:8});Progress({value:15,total:30,type:ProgressType.Linear}).color($r(sys.color.comp_background_emphasize)).height(8).width(100%);}.padding(16).borderRadius(16).backgroundColor($r(sys.color.background_primary));}当前示例中日期数据是硬编码的。在实际项目中应该通过aboutToAppear同步加载aboutToAppear():void{letplanLearningPlanManager.getInstance().getLearningPlan();letprogressLearningPlanManager.getInstance().getTodayProgress();this.learnedCountplan.todayWordCount;this.wordProgressprogress.wordProgress;this.continuousDaysplan.continuousDays;}四、业务数据与 UI 状态的分离一个重要的设计原则是业务数据与 UI 状态分离业务数据层 UI 状态层 ┌──────────────────┐ ┌──────────────────┐ │ LearningPlan │ ──→ │ Local learned │ │ .todayWordCount │ 读取 │ Count: number │ │ │ │ │ │ NewWordManager │ ──→ │ Local wordCount │ │ .getCount() │ 读取 │ : number │ │ │ │ │ │ PreferenceUtil │ ──→ │ onClick 事件中 │ │ .get(key) │ 直接 │ 实时读取 │ └──────────────────┘ └──────────────────┘典型场景——错题练习的点击事件展示了如何在交互发生时实时读取数据而非预加载myWrongPracticeEvent(){// 在点击时实时从 PreferenceUtil 读取letarr:TopicItemType[]PreferenceUtil.getInstance().get(PreferConstant.ERROR_RECORDS,[])asTopicItemType[];if(arr.length0){GlobalContextUtils.showToast({message:暂无易错词汇});return;}// 按时间戳排序最新的排最前面arr.sort((a,b)b.jlStamp-a.jlStamp);letrouterModel:QuestionsRouterModelnewQuestionsRouterModel();routerModel.sourceType错题练习;routerModel.datalistarr;RouterModule.push({url:RouterMap.MY_WRONG_PAGE,param:routerModel});}这种事件触发时读取的模式避免了维护数据的一致性状态特别适合页面跳转传递参数的场景。五、计算属性的数据聚合在某些场景下UI 需要展示的不只是原始数据而是基于原始数据计算出的派生数据。例如学习计划中的进度百分比// 在 LearningPlanManager 中封装计算逻辑publicgetTodayProgress():TodayProgress{letplanthis.getLearningPlan();return{wordProgress:plan.dailyWordGoal0?Math.min(100,Math.floor(plan.todayWordCount/plan.dailyWordGoal*100)):0,listeningProgress:plan.dailyListeningGoal0?Math.min(100,Math.floor(plan.todayListeningDuration/plan.dailyListeningGoal*100)):0,readingProgress:plan.dailyReadingGoal0?Math.min(100,Math.floor(plan.todayReadingDuration/plan.dailyReadingGoal*100)):0};}这样 UI 层只需要展示getTodayProgress()的结果而不需要关心百分比的计算逻辑。六、与 AppStorageV2 的配合全局用户信息通过AppStorageV2.connect连接实现跨页面状态同步// 连接用户信息自动同步登录/登出状态LocallogoUser:UserInfoAppStorageV2.connect(UserInfo,()newUserInfo())!;// 在 UI 中直接使用if(!this.logoUser.isLogin){// 展示未登录提示Image($r(app.media.ic_noLogin))Text(登录后享受更多服务)}else{// 展示学习进度 GridGrid(){/* ... */}}AppStorageV2的connect方法会将指定类型的状态与当前组件关联当全局状态发生变化时如用户登录/登出所有关联的组件自动刷新。七、最佳实践7.1 区分实时读取与状态订阅实时读取用于一次性数据如跳转传参在事件触发时直接从数据源读取。状态订阅用于 UI 中持续展示的数据如学习进度需要将数据同步到Local变量。7.2 使用 ViewModel 管理聚合逻辑当页面逻辑变得复杂时考虑引入 ViewModel 来封装数据聚合逻辑exportclassHomeVM{staticinstance:HomeVMnewHomeVM();gettodayProgress():TodayProgress{returnLearningPlanManager.getInstance().getTodayProgress();}getnewWordCount():number{returnNewWordManager.getInstance().getNewWordCount();}getstudySummary():StudySummary{returnLearningPlanManager.getInstance().getStudySummary();}}7.3 数据初始化时机aboutToAppear适合初始化页面级别的 UI 状态数据。build方法中仅做 UI 布局不要在 build 中执行耗时的数据加载操作。事件回调中适合在用户交互时实时读取数据。八、总结首页的数据聚合展示了 HarmonyOS 应用中多数据源协调的典型模式Manager 管理业务数据和持久化逻辑Local/Trace管理 UI 状态AppStorageV2管理跨页面全局状态Builder 方法按需加载数据。这种分层设计保持了各层的关注点分离使页面代码清晰、可维护同时也为后续的功能扩展提供了良好的架构基础。