HarmonyOS宠物邻里实战第5篇:通知中心、已读同步与AppStorage刷新闭环

📅 2026/7/5 8:45:57 👁️ 阅读次数 📝 编程学习
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.etsNotice、用户、帖子、寄养相关模型
MyApp/library2/src/main/ets/router点击通知后的路由跳转

项目视觉方向如下,通知中心和寄养、社区、宠物档案共享同一套 App 结构。

环境与验证信息

工程当前使用 HarmonyOS ArkTS / Stage 模型,入口模块支持phonetablet2in1。通知中心虽然是列表页,但会影响主壳红点、我的页统计和详情页跳转,因此不能只按单页功能处理。

项目
HarmonyOS 工程模型modelVersion: 6.0.2
target SDK6.0.2(22)
compatible SDK6.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'&&notice.type!==this.currentType){returnfalse;}if(this.unreadOnly&&notice.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()
已读后又变未读后端快照是否覆盖了本地状态
点击通知跳错页面targetTypetargetId是否正确
通知重复创建通知时是否按业务 ID 去重
列表不刷新数组是否重新赋值,ForEachkey 是否稳定
未读数不准是否只统计当前用户通知

这些问题通常不是 UI 问题,而是通知数据和刷新链路没设计清楚。

十九、验收清单

交付通知中心前,我会按这张表检查:

检查项通过标准
模型完整通知包含类型、目标、已读、时间和所属用户
创建集中评论、点赞、寄养动作通过状态层创建通知
已读同步单条已读和全部已读能同步 UI
红点刷新主壳、通知页、我的页未读数一致
路由正确点击通知进入正确详情页
后端兜底已读和通知列表能同步后端
空状态无通知、无未读、失败都有展示
安全边界通知不保存敏感完整对象

这张清单能防止通知中心只完成“列表能看”,但没有完成“业务闭环”。

总结

通知中心的核心是连接业务动作和用户反馈。比较稳的分层是:

业务页面:触发评论、点赞、寄养申请等动作 MockStore:创建通知、标记已读、刷新版本号 BackendService:同步通知和已读状态 NoticeTab:展示、筛选、打开通知 RouterService:根据 targetType + targetId 跳转

对 HarmonyOS/ArkTS 项目来说,MockStore + AppStorage是一套轻量但够用的通知刷新方案。它不需要引入复杂状态库,就能保证通知页、主壳红点和业务详情页之间保持一致。

通知中心做好以后,整个 App 会更像一个完整产品:用户发起动作后有反馈,别人和自己产生互动后能收到提醒,业务状态变化后也能被及时看见。这就是通知模块真正的价值。