HarmonyOS宠物邻里实战第5篇:通知中心、已读同步与AppStorage刷新闭环
HarmonyOS宠物邻里实战第5篇:通知中心、已读同步与AppStorage刷新闭环
摘要
通知中心是移动 App 里很容易被低估的模块。它看起来只是一个列表,但真正放到宠物邻里项目里,会同时连接社区评论、点赞收藏、寄养申请、寄养状态变化、系统提醒、账号安全和用户中心未读数。如果没有统一设计,通知会散落到各个页面,最后出现“业务发生了,但通知没刷新”“已读了列表还显示红点”“详情页状态变了通知页不知道”的问题。
本文基于宠物邻里 HarmonyOS 项目,复盘通知中心的工程设计:
Notice模型如何设计;- 通知类型和业务来源如何拆分;
MockStore如何集中创建、标记已读和清空通知;AppStorage版本号如何驱动通知页、我的页和主壳红点刷新;BackendService如何同步后端;- 评论、点赞、寄养申请和系统消息如何接入通知中心;
- 交付前如何验证已读状态、未读数和跨页面刷新。
文章重点不是“写一个列表 UI”,而是把通知当成一个跨业务模块的数据同步入口来处理。
工程背景与源码定位
宠物邻里 App 包含宠物档案、社区动态、寄养互助、提醒、通知和个人中心。通知中心位于主 Tab 中,既要展示消息列表,也要承担未读数汇总、已读状态同步和跳转入口。
本文涉及的文件如下:
| 文件 | 作用 |
|---|---|
MyApp/entry/src/main/ets/pages/notice/NoticeTab.ets | 通知列表页,展示筛选、未读数和消息项 |
MyApp/entry/src/main/ets/pages/notice/NoticeDetailPage.ets | 通知详情页 |
MyApp/entry/src/main/ets/components/notice/NoticeListItem.ets | 通知列表项组件 |
MyApp/entry/src/main/ets/common/MockStore.ets | 本地通知数据、已读操作和刷新版本 |
MyApp/entry/src/main/ets/services/BackendService.ets | 后端同步通知已读状态 |
MyApp/library2/src/main/ets/models/Models.ets | Notice、用户、帖子、寄养相关模型 |
MyApp/library2/src/main/ets/router | 点击通知后的路由跳转 |
项目视觉方向如下,通知中心和寄养、社区、宠物档案共享同一套 App 结构。
环境与验证信息
工程当前使用 HarmonyOS ArkTS / Stage 模型,入口模块支持phone、tablet、2in1。通知中心虽然是列表页,但会影响主壳红点、我的页统计和详情页跳转,因此不能只按单页功能处理。
| 项目 | 值 |
|---|---|
| HarmonyOS 工程模型 | modelVersion: 6.0.2 |
| target SDK | 6.0.2(22) |
| compatible SDK | 6.0.2(22) |
| 状态同步 | MockStore + AppStorage |
| 后端框架 | Express |
| 数据访问 | MongoDB Driver |
后端验证命令:
cd D:\APP\chong_wu_guan_li\houduan\test npm run check npm run test:integration集成测试覆盖寄养留言、通知、坐标、状态时间线和评价,说明通知不是孤立列表,而是业务动作的一部分。
一、通知中心要解决什么问题
通知中心至少要解决四件事:
| 问题 | 说明 |
|---|---|
| 消息聚合 | 评论、点赞、寄养申请、系统消息统一展示 |
| 已读同步 | 列表、详情页、主壳红点状态一致 |
| 路由跳转 | 点击通知后进入对应帖子、寄养需求或系统页 |
| 业务解耦 | 业务模块只创建通知,不关心通知页怎么展示 |
如果把通知当成“每个页面自己弹 Toast”,后期很快会失控。通知中心应该是统一消息仓库,而不是页面临时提示的集合。
二、Notice 模型设计
通知模型可以这样设计:
enumNoticeType{Comment='comment',Like='like',Favorite='favorite',Foster='foster',Reminder='reminder',System='system'}interfaceNotice{id:string;userId:string;type:NoticeType;title:string;content:string;targetType:string;targetId:string;actorId?:string;read:boolean;createdAt:number;}字段设计的重点:
userId:这条通知属于谁;type:通知展示分类;targetType + targetId:点击后跳转哪里;actorId:谁触发了这条通知;read:是否已读;createdAt:排序和时间展示。
不要把完整帖子、完整寄养需求或完整用户对象塞进通知里。通知只保存跳转所需的引用信息。
三、通知类型与业务来源
通知类型可以和业务来源对应:
| 类型 | 来源 | 目标页面 |
|---|---|---|
comment | 帖子被评论、评论被回复 | 帖子详情或评论详情 |
like | 帖子被点赞 | 帖子详情 |
favorite | 帖子被收藏 | 帖子详情 |
foster | 寄养申请、通过、拒绝、开始、完成 | 寄养详情或寄养记录 |
reminder | 宠物喂养、疫苗、驱虫提醒 | 提醒页或宠物详情 |
system | 账号、安全、平台公告 | 系统详情页 |
这样 UI 可以按type展示不同图标、标签色和筛选项,路由层可以按targetType决定跳转。
四、MockStore 集中管理通知
通知数据可以由MockStore统一维护:
staticnotices:Notice[]=MockStore.seedNotices();提供查询方法:
staticmyNotices():Notice[]{returnMockStore.notices.filter((notice:Notice)=>notice.userId===MockStore.meId).sort((a:Notice,b:Notice)=>b.createdAt-a.createdAt);}提供未读数:
staticunreadNoticeCount():number{returnMockStore.myNotices().filter((notice:Notice)=>!notice.read).length;}页面不直接遍历全局数组,而是调用这些语义方法。
五、创建通知不要散落在页面里
当用户评论帖子时,页面只负责提交评论:
MockStore.createComment(comment);状态层内部可以创建通知:
staticcreatePostComment(comment:PostComment):void{MockStore.comments=MockStore.comments.concat([comment]);MockStore.createNotice({userId:comment.postOwnerId,type:NoticeType.Comment,title:'收到新的评论',content:comment.content,targetType:'post',targetId:comment.postId,actorId:comment.userId});MockStore.bumpPostsVersion();MockStore.bumpNoticeVersion();}业务动作和通知副作用放在同一层处理,才能保证不会漏。
六、寄养业务如何接入通知
寄养申请提交时:
申请者提交申请 -> 创建 FosterApplication -> 给需求发布者创建 foster 通知 -> bumpFosterVersion -> bumpNoticeVersion申请通过时:
发布者通过申请 -> 申请者收到通过通知 -> 其他待处理申请者收到未通过通知 -> 创建寄养记录 -> 需求状态更新 -> 通知中心刷新通知内容可以简洁:
MockStore.createNotice({userId:application.userId,type:NoticeType.Foster,title:'寄养申请已通过',content:'请在约定时间完成接送确认',targetType:'fosterRecord',targetId:record.id,actorId:request.ownerId});这里的targetType指向记录而不是申请,因为用户下一步更关心履约记录。
七、已读操作设计
通知已读有三种常见动作:
| 动作 | 场景 |
|---|---|
| 单条已读 | 点击某条通知 |
| 全部已读 | 通知页右上角按钮 |
| 按类型已读 | 只清空评论、只清空系统消息 |
项目早期可以先实现单条和全部:
staticmarkNoticeRead(id:string):void{MockStore.notices=MockStore.notices.map((notice:Notice)=>{if(notice.id!==id){returnnotice;}return{...notice,read:true};});MockStore.bumpNoticeVersion();BackendService.markNoticeRead(id,MockStore.meId).catch((e:Error)=>{MockStore.reportSyncFailure('通知已读同步失败,稍后会重试',e);});}注意这里使用数组重新赋值,而不是直接修改对象字段,能减少 ArkTS 页面刷新不及时的问题。
八、全部已读
全部已读可以这样写:
staticmarkAllNoticesRead():void{MockStore.notices=MockStore.notices.map((notice:Notice)=>{if(notice.userId!==MockStore.meId){returnnotice;}return{...notice,read:true};});MockStore.bumpNoticeVersion();BackendService.markAllNoticesRead(MockStore.meId).catch((e:Error)=>{MockStore.reportSyncFailure('全部已读同步失败,稍后会重试',e);});}这样通知页、主 Tab 红点和我的页统计都能刷新。
九、AppStorage 版本号驱动刷新
通知变化后写入版本号:
staticbumpNoticeVersion():void{constv:number=AppStorage.get<number>('noticeVersion')??0;AppStorage.setOrCreate<number>('noticeVersion',v+1);}通知页监听:
@StorageLink('noticeVersion')@Watch('refresh')noticeVersion:number=0;主壳或我的页也可以监听:
@StorageLink('noticeVersion')@Watch('refreshBadge')noticeVersion:number=0;这样一个通知已读后,列表页和红点能同步变化,不需要页面之间互相调用。
十、通知页状态组织
通知页可以维护筛选状态:
@Statenotices:Notice[]=[];@StatecurrentType:string='all';@StateunreadOnly:boolean=false;刷新时从MockStore取数据:
privaterefresh():void{this.notices=MockStore.myNotices();}筛选时只处理页面展示:
privatefiltered():Notice[]{returnthis.notices.filter((notice:Notice)=>{if(this.currentType!=='all'&¬ice.type!==this.currentType){returnfalse;}if(this.unreadOnly&¬ice.read){returnfalse;}returntrue;});}通知页不负责创建通知,只负责展示和触发已读。
十一、列表项组件边界
NoticeListItem可以接收通知对象和点击回调:
@Componentexportstruct NoticeListItem{@Propnotice:Notice;onTap:()=>void=()=>{};build(){Row(){Column(){Text(this.notice.title)Text(this.notice.content)}if(!this.notice.read){Circle().width(8).height(8)}}.onClick(()=>this.onTap())}}组件不直接调用MockStore.markNoticeRead,否则复用到不同场景时会被固定行为限制。点击后的业务动作交给页面处理。
十二、点击通知后的路由
点击通知时,通常先标记已读,再跳转:
privateopenNotice(notice:Notice):void{MockStore.markNoticeRead(notice.id);this.routeByNotice(notice);}路由映射:
privaterouteByNotice(notice:Notice):void{if(notice.targetType==='post'){RouterService.push(RouteName.PostDetail,{id:notice.targetId});return;}if(notice.targetType==='fosterRequest'){RouterService.push(RouteName.FosterRequestDetail,{id:notice.targetId});return;}if(notice.targetType==='fosterRecord'){RouterService.push(RouteName.FosterRecordDetail,{id:notice.targetId});return;}}这里不要把路由写死在通知列表项组件里,页面层更适合处理路由。
十三、红点和未读数
未读数来自MockStore.unreadNoticeCount():
constcount=MockStore.unreadNoticeCount();主 Tab 可以显示:
count > 99 -> 99+ count > 0 -> count count = 0 -> 不显示未读数不要由通知页单独维护,否则用户在详情页已读后,主壳红点可能不更新。
十四、后端同步策略
通知已读通常可以采用乐观更新:
本地先标记已读 -> 刷新 UI -> 后端同步 -> 失败后提示稍后重试原因是已读状态不是高风险业务,短时间本地状态领先后端是可以接受的。下次刷新快照时再以服务端为准。
但创建通知最好由后端也持久化。否则用户换设备后会丢消息。
十五、快照刷新
登录或下拉刷新时,可以从后端加载通知快照:
statichydrate(snapshot:RemoteSnapshot):void{MockStore.notices=snapshot.notices;MockStore.bumpNoticeVersion();}如果通知中心支持分页,就不要一次性覆盖全部通知,可以按页合并:
MockStore.notices=mergeById(MockStore.notices,incomingNotices);早期项目数据量小,完整快照更简单;后续通知多了再做分页和增量同步。
十六、空状态和加载失败
通知页至少要有三个状态:
| 状态 | 展示 |
|---|---|
| 无通知 | “暂无消息” |
| 只有已读 | “没有未读消息” |
| 加载失败 | “消息同步失败,可稍后重试” |
空状态不是装饰,它能减少用户误解。尤其是“未读筛选”下没有内容时,应该告诉用户只是没有未读,而不是整个通知中心为空。
十七、响应式布局
通知列表也要考虑多设备:
| 设备 | 布局 |
|---|---|
| 手机 | 单列列表,顶部筛选横向滚动 |
| 平板 | 列表宽度居中,详情页可保持大卡片 |
| 2in1 | 左侧列表、右侧详情预览也可以作为后续增强 |
项目里可以复用Responsive.contentWidth()和Responsive.pagePadding():
.width(Responsive.contentWidth(this.pageWidth)).padding({left:Responsive.pagePadding(this.pageWidth),right:Responsive.pagePadding(this.pageWidth)})通知列表是高频阅读页面,宽屏下不要无限拉长行宽。
十八、常见问题排查
| 问题 | 排查点 |
|---|---|
| 红点不消失 | 是否调用bumpNoticeVersion() |
| 已读后又变未读 | 后端快照是否覆盖了本地状态 |
| 点击通知跳错页面 | targetType和targetId是否正确 |
| 通知重复 | 创建通知时是否按业务 ID 去重 |
| 列表不刷新 | 数组是否重新赋值,ForEachkey 是否稳定 |
| 未读数不准 | 是否只统计当前用户通知 |
这些问题通常不是 UI 问题,而是通知数据和刷新链路没设计清楚。
十九、验收清单
交付通知中心前,我会按这张表检查:
| 检查项 | 通过标准 |
|---|---|
| 模型完整 | 通知包含类型、目标、已读、时间和所属用户 |
| 创建集中 | 评论、点赞、寄养动作通过状态层创建通知 |
| 已读同步 | 单条已读和全部已读能同步 UI |
| 红点刷新 | 主壳、通知页、我的页未读数一致 |
| 路由正确 | 点击通知进入正确详情页 |
| 后端兜底 | 已读和通知列表能同步后端 |
| 空状态 | 无通知、无未读、失败都有展示 |
| 安全边界 | 通知不保存敏感完整对象 |
这张清单能防止通知中心只完成“列表能看”,但没有完成“业务闭环”。
总结
通知中心的核心是连接业务动作和用户反馈。比较稳的分层是:
业务页面:触发评论、点赞、寄养申请等动作 MockStore:创建通知、标记已读、刷新版本号 BackendService:同步通知和已读状态 NoticeTab:展示、筛选、打开通知 RouterService:根据 targetType + targetId 跳转对 HarmonyOS/ArkTS 项目来说,MockStore + AppStorage是一套轻量但够用的通知刷新方案。它不需要引入复杂状态库,就能保证通知页、主壳红点和业务详情页之间保持一致。
通知中心做好以后,整个 App 会更像一个完整产品:用户发起动作后有反馈,别人和自己产生互动后能收到提醒,业务状态变化后也能被及时看见。这就是通知模块真正的价值。
