【听见课堂 HarmonyOS NEXT 实战系列 03】ArkUI 页面为什么不该直接操作数据库:Service 与 Repository 分层实战

【听见课堂 HarmonyOS NEXT 实战系列 03】ArkUI 页面为什么不该直接操作数据库:Service 与 Repository 分层实战 【听见课堂 HarmonyOS NEXT 实战系列 03】ArkUI 页面为什么不该直接操作数据库Service 与 Repository 分层实战在 ArkUI 项目中直接在页面生命周期里打开数据库、查询列表、筛选结果再顺手更新计数看起来是最快的实现方式。问题是一旦课程首页、复习页、任务中心和历史记录都需要同一批数据页面就会逐渐拥有多套不同的业务口径。听见课堂采用ArkUI Page → Service → Repository → RelationalStore的分层方式。页面负责展示和交互草稿态Service 负责业务规则与聚合Repository 负责数据源读写。下面结合项目中的真实接口拆解这条数据链路。一、页面直接查库会带来什么问题假设复习页需要展示“待确认字幕”和“待确认扫描”任务中心需要展示“今天到期”“已逾期”“已完成”。如果每个页面都直接查询数据库很快会出现这些问题页面重复拼装查询条件字段口径容易不一致保存任务后要手工更新多个页面的角标与统计数字数据库异常、空库和迁移失败被迫在 UI 中处理单元测试必须创建页面和系统上下文难以隔离业务规则未来切换内存数据、测试数据或远程数据时页面需要大面积修改。更稳妥的做法是让页面只知道“我要一个复习快照”或“我要更新这条任务”而不关心底层使用了哪张表。二、Repository 用接口隔离数据源听见课堂首先定义ClassroomRepository接口把课程、字幕、扫描和任务的读写动作集中起来exportinterfaceClassroomRepository{getTodayCourse():PromiseCourseSummary;getTranscript():PromiseArrayTranscriptSegment;replaceTranscript(courseId:string,transcript:ArrayTranscriptSegment):Promiseboolean;getScanNotes():PromiseArrayScanNote;getTasks():PromiseArrayTaskItem;updateCourse(course:CourseSummary):Promiseboolean;updateScanText(scanId:string,text:string,source?:string):Promiseboolean;updateTask(taskId:string,title:string,dueText:string,dueAtMillis:number):Promiseboolean;confirmTask(taskId:string):Promiseboolean;setTaskCompleted(taskId:string,completed:boolean):Promiseboolean;}接口的价值不只是“面向接口编程”。它明确了数据层的职责边界Repository 返回领域模型不返回页面组件需要的临时颜色、按钮文案或弹窗状态。项目同时可以拥有RelationalClassroomRepository和内存实现。前者用于真实本地数据后者可以作为能力不可用时的降级或测试替身。上层 Service 不需要知道当前使用的是哪种实现。三、Service 负责业务聚合不只是转发调用如果 Service 只是把 Repository 的方法原样包一层分层并没有产生真正价值。听见课堂在 Service 中完成确认状态筛选、任务统计和能力状态归一化。例如任务中心需要的内容可以一次聚合为页面快照asyncgetTaskCenterSnapshot():PromiseTaskCenterSnapshot{consttasks:ArrayTaskItemawaitthis.repository.getTasks();constnowMillis:numberDate.now();constitems:ArrayTaskCenterItemtasks.filter((task:TaskItem)task.confirmed).map((task:TaskItem):TaskCenterItem{constdueAtMillistask.dueAtMillis0?task.dueAtMillis:TaskDateResolver.resolveDueText(task.dueText,nowMillis);constoverdueTaskDateResolver.isOverdue(dueAtMillis,task.completed,nowMillis);conststatustask.completed?已完成:(overdue?已过期:待办);returnnewTaskCenterItem(task.id,task.title,task.dueText,task.source,status,TaskDateResolver.dateGroup(dueAtMillis,nowMillis),task.completed,overdue,,dueAtMillis,TaskDateResolver.dateKey(dueAtMillis));});// 后续统一排序并计算 pending、completed、overdue 与日历统计}任务中心也可以由 Service 统一计算“今日”“逾期”“已完成”等分组。这样手机底部导航角标、平板侧栏统计和任务中心列表都基于同一份 canonical data而不是在三个页面中各自加减计数。一个重要原则是写入完成后重新读取并聚合权威数据不要手工猜测其他页面应该变成什么状态。四、初始化逻辑应该只有一个入口应用启动时EntryAbility调用统一初始化方法而不是自己创建数据库exportasyncfunctioninitializeClassroomData(context:common.UIAbilityContext):Promiseboolean{try{constrepositorynewRelationalClassroomRepository();awaitrepository.initialize(context);classroomService.useRepository(repository,RelationalStore);returntrue;}catch(error){classroomService.useRepository(fallbackRepository,内存降级);returnfalse;}}具体实现可以先尝试创建 RelationalStore Repository失败时切换到内存实现并记录原因。对页面而言只要 Service 注册完成它就能通过稳定接口加载数据。这也让失败路径更容易解释初始化成功页面加载本地持久化数据数据库不可用但降级成功页面仍可操作但应提示数据不会持久保存Service 未初始化页面进入明确错误态提供重试而不是一直显示 loading。五、页面只负责消费状态和触发动作在页面层推荐把一次刷新写成清晰的状态转换privateasyncrefreshData():Promisevoid{this.isDataLoadingtrue;this.dataError;try{this.courseawaitthis.service.getTodayCourse();this.transcriptawaitthis.service.getTranscript();this.scanNotesawaitthis.service.getScanNotes();this.candidateTasksawaitthis.service.getCandidateTasks();this.confirmedTasksawaitthis.service.getConfirmedTasks();this.reviewSnapshotawaitthis.service.getReviewSnapshot();this.taskCenterSnapshotawaitthis.service.getTaskCenterSnapshot();}catch(error){this.dataError课堂数据暂时不可用请稍后重试。;}finally{this.isDataLoadingfalse;}}页面仍然需要处理loading、empty、error、disabled和pressed但它不负责解释数据库错误码也不负责决定什么叫“待确认”。当用户确认一段字幕时页面触发 Service 动作动作完成后重新请求复习快照。即使另一个页面或后台流程修改了数据页面也会回到 Repository 中的真实结果。六、RelationalStore 负责结构化数据与迁移字幕、扫描、任务都具备列表、筛选、排序和关联关系适合使用 RelationalStore。Repository 实现需要额外关注首次安装时建表与种子数据schema 版本升级与迁移空库、重复主键和脏数据兜底多步写入时的事务一致性将数据库行映射回显式 ArkTS 类型异常转换成上层可理解的错误而不是直接把底层对象抛给页面。用户设置、主题开关或少量最近状态则更适合 Preferences。不要因为 Preferences 调用简单就把任务列表序列化成一个越来越大的字符串。七、分层之后如何测试这套结构可以把验证拆开Repository 测试验证建表、增删改查、排序和迁移Service 测试注入内存 Repository验证确认筛选、任务统计和边界日期页面测试验证加载、空态、错误态和交互反馈真机验证验证 RelationalStore、生命周期恢复和数据持久化。构建成功只能说明类型、资源和打包链路基本成立不能替代数据库迁移测试单元测试通过也不能替代真机生命周期验证。发布文章时同样要如实区分这些证据层级。总结Page → Service → Repository不是为了增加文件数量而是为了让每一层只回答一种问题页面决定怎么显示Service 决定业务规则Repository 决定数据如何读写。下一篇将继续向下看领域数据本身分析Course、Transcript、Scan和Task如何组成一条可追溯的课堂证据链。