AtomGit Flutter鸿蒙客户端:通知系统 功能定位与当前状态通知功能处于架构规划阶段。Tab 页面已在底部导航栏中创建但后端功能尚未接入目前展示占位 UI。这种先建框架、后接数据的开发方式允许早期用户就能看到应用的功能蓝图也为后续开发预留了完整的代码骨架。通知在 Git 平台中扮演着核心的信息聚合角色。开发者的日常协作——有人给你的 Issue 评论了、有人 Star 了你的仓库、有人发起了 PR ——都通过通知机制传递。一个好的通知系统能够显著提高开发者的响应效率。Auth-Aware UI身份驱动的界面切换通知 Tab 是一个典型的 Auth-Aware 组件——其 UI 完全由登录状态决定。未登录时展示引导界面已登录时展示功能内容当前为占位。这种设计遵循根据用户状态提供合适界面的原则。classNotificationsTabextendsStatelessWidget{overrideWidgetbuild(BuildContextcontext){finalisLoggedIncontext.watchAuthProvider().isLoggedIn;returnScaffold(appBar:AppBar(title:constText(通知)),body:isLoggedIn?_buildPlaceholder(context):_buildLoginPrompt(context),);}}context.watchAuthProvider()建立了对 AuthProvider 的持续订阅。当用户完成登录或退出登录时AuthProvider 调用notifyListeners()此 Widget 自动重建无需手动刷新。这是 Provider 框架的核心价值——通过声明式依赖实现自动 UI 同步。未登录引导的设计登录引导是用户进入未登录 Tab 时看到的第一个界面。设计上需要传达三个层次的信息这个功能是什么、为什么需要登录、如何登录Widget_buildLoginPrompt(BuildContextcontext){returnCenter(child:Padding(padding:constEdgeInsets.all(32),child:Column(mainAxisAlignment:MainAxisAlignment.center,children:[// 第一层视觉焦点 —— 大尺寸功能图标Icon(Icons.notifications_outlined,size:80,color:Colors.grey[400],),constSizedBox(height:16),// 第二层功能说明 —— 做什么Text(登录后可查看通知,style:Theme.of(context).textTheme.titleMedium,),constSizedBox(height:8),// 第三层价值主张 —— 为什么值得登录Text(关注仓库动态、Issue 讨论和 PR 更新,textAlign:TextAlign.center,style:Theme.of(context).textTheme.bodyMedium?.copyWith(color:Colors.grey,),),constSizedBox(height:24),// 第四层行动引导 —— 点击登录FilledButton.icon(onPressed:()Navigator.pushNamed(context,/login),icon:constIcon(Icons.login),label:constText(立即登录),),],),),);}引导页面的视觉层次设计大图标80px灰色调——建立视觉焦点暗示功能属性标题文案titleMedium醒目的字号——一句话说明功能价值描述文案bodyMedium灰色文字——补充功能细节登录按钮FilledButtonMaterial 3 填充样式——明确的行动号召图标使用Icons.notifications_outlinedoutlined 风格而非 filled与 Tab 未选中状态的图标保持视觉一致性。灰色调传达功能尚未激活的语义。已登录占位 UI功能尚未实现时占位 UI 承担着管理用户期望的作用Widget_buildPlaceholder(BuildContextcontext){returnCenter(child:Column(mainAxisAlignment:MainAxisAlignment.center,children:[Icon(Icons.construction,size:64,color:Colors.grey[400],),constSizedBox(height:16),Text(通知功能即将上线,style:Theme.of(context).textTheme.titleMedium,),constSizedBox(height:8),Text(敬请期待,style:Theme.of(context).textTheme.bodyMedium?.copyWith(color:Colors.grey,),),],),);}使用Icons.construction施工图标明确表达正在建设中的含义用户一看就理解这不是 Bug 而是功能尚未完成。通知系统的完整设计规划数据模型AtomGit 的通知以 Thread 为单位组织每个 Thread 对应一个事件主题classNotificationItem{finalStringid;finalStringtype;finalStringrepoFullName;finalStringsubject;finalbool unread;finalDateTimeupdatedAt;finalString?reason;// 触发原因mention, assign, author 等finalString?subjectUrl;// 相关 Issue/PR 的 API URL}通知类型枚举enumNotificationType{star,// 有人 Star 了你的仓库issue,// Issue 有更新新评论、状态变更pullRequest,// PR 有更新mention,// 有人 提及你assigned,// 你被分配了 Issueforked,// 有人 Fork 了你的仓库}Provider 骨架classNotificationProviderextendsChangeNotifier{finalAtomGitApiClient_apiClient;ListNotificationItem_notifications[];int _unreadCount0;bool _isLoadingfalse;String?_error;int _page1;bool _hasMorefalse;ListNotificationItemgetnotificationsList.unmodifiable(_notifications);intgetunreadCount_unreadCount;boolgetisLoading_isLoading;String?geterror_error;boolgethasMore_hasMore;Futurevoidload()async{_page1;_isLoadingtrue;_errornull;notifyListeners();try{finalresponseawait_apiClient.get(/notifications,queryParams:{all:true,per_page:30,page:1,},);finalitemsparseListdynamic(response.data)??[];_notificationsitems.whereTypeMapString,dynamic().map(_parseNotification).toList();_hasMore_notifications.length30;// 从未读计数 Header 更新_updateUnreadCount(response);}onApiExceptioncatch(e){_errore.message;}finally{_isLoadingfalse;notifyListeners();}}FuturevoidmarkAsRead(StringthreadId)async{try{await_apiClient.patch(/notifications/threads/$threadId);// 本地更新状态finalindex_notifications.indexWhere((n)n.idthreadId);if(index!-1){_notifications[index]_notifications[index].copyWith(unread:false);_unreadCount_unreadCount0?_unreadCount-1:0;notifyListeners();}}onApiException{// 静默失败——标记已读失败不影响浏览}}FuturevoidmarkAllAsRead()async{try{await_apiClient.put(/notifications);_notifications_notifications.map((n)n.copyWith(unread:false)).toList();_unreadCount0;notifyListeners();}onApiException{// 静默失败}}}标记已读操作采用乐观更新策略先更新本地 UI 状态立即可见再请求 API。如果 API 失败本地状态已变更用户会短暂看到已读但刷新后又恢复未读。为了简化当前设计采用悲观更新——等 API 成功后再更新本地状态。轮询策略设计AtomGit API 不提供 WebSocket移动端通知更新需要轮询策略间隔电量消耗实时性适用场景前台短轮询30s中等较高应用前台时前台长轮询5min低低应用前台闲置时后台轮询不轮询无无切换到后台时推荐的轮询方案应用前台时 60 秒间隔轮询未读计数有未读通知时才拉取完整列表。切换到后台时停止轮询HarmonyOS 的后台任务限制也会自然终止轮询。classNotificationProviderextendsChangeNotifier{Timer?_pollTimer;voidstartPolling(){_pollTimer?.cancel();_pollTimerTimer.periodic(constDuration(seconds:60),(_)_pollUnreadCount(),);}voidstopPolling(){_pollTimer?.cancel();_pollTimernull;}Futurevoid_pollUnreadCount()async{try{finalresponseawait_apiClient.get(/notifications,queryParams:{all:false,per_page:1},);// 从响应头提取未读计数}onApiException{// 轮询失败静默忽略}}overridevoiddispose(){stopPolling();super.dispose();}}Tab 角标实现MainShell 的底部导航栏支持通知角标NavigationDestination(icon:_buildNotificationIcon(false),selectedIcon:_buildNotificationIcon(true),label:通知,)Widget_buildNotificationIcon(bool selected){finalunreadCountcontext.watchNotificationProvider().unreadCount;if(unreadCount0){returnBadge(label:Text(unreadCount99?99:$unreadCount,style:constTextStyle(fontSize:10),),child:Icon(selected?Icons.notifications:Icons.notifications_outlined),);}returnIcon(selected?Icons.notifications:Icons.notifications_outlined);}Material 3 的Badge组件提供标准的角标样式。超过 99 的未读数显示为 “99” 避免角标过大。通知列表 UI 规划Widget_buildNotificationList(BuildContextcontext){finalprovidercontext.watchNotificationProvider();if(provider.error!nullprovider.notifications.isEmpty){returnErrorRetryWidget(message:provider.error!,onRetry:()provider.load(),);}if(provider.notifications.isEmpty!provider.isLoading){returnconstCenter(child:Text(暂无通知));}returnListView.builder(itemCount:provider.notifications.length(provider.hasMore?1:0),itemBuilder:(context,index){if(indexprovider.notifications.length){returnconstCenter(child:CircularProgressIndicator());}return_NotificationTile(notification:provider.notifications[index],onTap:()_handleNotificationTap(provider.notifications[index],),);},);}每条通知的设计class_NotificationTileextendsStatelessWidget{finalNotificationItemnotification;finalVoidCallbackonTap;Widgetbuild(BuildContextcontext){returnListTile(leading:_buildTypeIcon(notification.type),title:Text(notification.subject,maxLines:2),subtitle:Row(children:[Text(notification.repoFullName),constText( · ),Text(DateFormatter.relative(notification.updatedAt)),]),tileColor:notification.unread?Theme.of(context).colorScheme.primaryContainer.withOpacity(0.3):null,onTap:onTap,);}Widget_buildTypeIcon(Stringtype){returnswitch(type){starconstIcon(Icons.star,color:Colors.amber),issueconstIcon(Icons.error_outline,color:Colors.green),pullconstIcon(Icons.call_split,color:Colors.blue),mentionconstIcon(Icons.alternate_email,color:Colors.purple),assignedconstIcon(Icons.assignment_ind,color:Colors.orange),_constIcon(Icons.notifications,color:Colors.grey),};}}通知的交互设计点击通知后的导航逻辑void_handleNotificationTap(NotificationItemnotification){// 标记为已读context.readNotificationProvider().markAsRead(notification.id);// 根据类型跳转switch(notification.type){casestar:caseforked:// 跳转到仓库详情finalpartsnotification.repoFullName.split(/);Navigator.pushNamed(context,/repo,arguments:{owner:parts[0],name:parts[1],});break;caseissue:casemention:caseassigned:// 跳转到 Issue 详情Navigator.pushNamed(context,/repo/issues/detail,arguments:_extractIssueArgs(notification));break;casepullRequest:// 跳转到 PR 详情Navigator.pushNamed(context,/repo/pulls/detail,arguments:_extractIssueArgs(notification));break;}}性能考量通知系统需要处理的性能问题未读计数。不应每次切换 Tab 都发起 API 请求。未读计数缓存在NotificationProvider._unreadCount中通过轮询更新。Tab 切换时的 UI 更新是纯本地操作从内存读取。列表分页。通知列表支持无限滚动每页 30 条。首次加载只获取最新一页。用户向下滚动时按需加载历史通知。后台同步。应用在后台时不进行网络请求。用户回到前台时立即触发一次未读计数更新。