AtomGit Flutter鸿蒙客户端:通知系统
2026/6/10 17:57:32 网站建设 项目流程

功能定位与当前状态

通知功能处于架构规划阶段。Tab 页面已在底部导航栏中创建,但后端功能尚未接入,目前展示占位 UI。这种"先建框架、后接数据"的开发方式允许早期用户就能看到应用的功能蓝图,也为后续开发预留了完整的代码骨架。

通知在 Git 平台中扮演着核心的信息聚合角色。开发者的日常协作——有人给你的 Issue 评论了、有人 Star 了你的仓库、有人发起了 PR ——都通过通知机制传递。一个好的通知系统能够显著提高开发者的响应效率。

Auth-Aware UI:身份驱动的界面切换

通知 Tab 是一个典型的 Auth-Aware 组件——其 UI 完全由登录状态决定。未登录时展示引导界面,已登录时展示功能内容(当前为占位)。这种设计遵循"根据用户状态提供合适界面"的原则。

classNotificationsTabextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){finalisLoggedIn=context.watch<AuthProvider>().isLoggedIn;returnScaffold(appBar:AppBar(title:constText('通知')),body:isLoggedIn?_buildPlaceholder(context):_buildLoginPrompt(context),);}}

context.watch<AuthProvider>()建立了对 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('立即登录'),),],),),);}

引导页面的视觉层次设计:

  1. 大图标(80px,灰色调)——建立视觉焦点,暗示功能属性
  2. 标题文案(titleMedium,醒目的字号)——一句话说明功能价值
  3. 描述文案(bodyMedium,灰色文字)——补充功能细节
  4. 登录按钮(FilledButton,Material 3 填充样式)——明确的行动号召

图标使用Icons.notifications_outlined(outlined 风格)而非 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;List<NotificationItem>_notifications=[];int _unreadCount=0;bool _isLoading=false;String?_error;int _page=1;bool _hasMore=false;List<NotificationItem>getnotifications=>List.unmodifiable(_notifications);intgetunreadCount=>_unreadCount;boolgetisLoading=>_isLoading;String?geterror=>_error;boolgethasMore=>_hasMore;Future<void>load()async{_page=1;_isLoading=true;_error=null;notifyListeners();try{finalresponse=await_apiClient.get('/notifications',queryParams:{'all':'true','per_page':'30','page':'1',},);finalitems=parseList<dynamic>(response.data)??[];_notifications=items.whereType<Map<String,dynamic>>().map(_parseNotification).toList();_hasMore=_notifications.length>=30;// 从未读计数 Header 更新_updateUnreadCount(response);}onApiExceptioncatch(e){_error=e.message;}finally{_isLoading=false;notifyListeners();}}Future<void>markAsRead(StringthreadId)async{try{await_apiClient.patch('/notifications/threads/$threadId');// 本地更新状态finalindex=_notifications.indexWhere((n)=>n.id==threadId);if(index!=-1){_notifications[index]=_notifications[index].copyWith(unread:false);_unreadCount=_unreadCount>0?_unreadCount-1:0;notifyListeners();}}onApiException{// 静默失败——标记已读失败不影响浏览}}Future<void>markAllAsRead()async{try{await_apiClient.put('/notifications');_notifications=_notifications.map((n)=>n.copyWith(unread:false)).toList();_unreadCount=0;notifyListeners();}onApiException{// 静默失败}}}

标记已读操作采用"乐观更新"策略:先更新本地 UI 状态(立即可见),再请求 API。如果 API 失败,本地状态已变更,用户会短暂看到"已读"但刷新后又恢复"未读"。为了简化,当前设计采用"悲观更新"——等 API 成功后再更新本地状态。

轮询策略设计

AtomGit API 不提供 WebSocket,移动端通知更新需要轮询:

策略间隔电量消耗实时性适用场景
前台短轮询30s中等较高应用前台时
前台长轮询5min应用前台闲置时
后台轮询不轮询切换到后台时

推荐的轮询方案:应用前台时 60 秒间隔轮询未读计数,有未读通知时才拉取完整列表。切换到后台时停止轮询(HarmonyOS 的后台任务限制也会自然终止轮询)。

classNotificationProviderextendsChangeNotifier{Timer?_pollTimer;voidstartPolling(){_pollTimer?.cancel();_pollTimer=Timer.periodic(constDuration(seconds:60),(_)=>_pollUnreadCount(),);}voidstopPolling(){_pollTimer?.cancel();_pollTimer=null;}Future<void>_pollUnreadCount()async{try{finalresponse=await_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){finalunreadCount=context.watch<NotificationProvider>().unreadCount;if(unreadCount>0){returnBadge(label:Text(unreadCount>99?'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){finalprovider=context.watch<NotificationProvider>();if(provider.error!=null&&provider.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(index>=provider.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){'star'=>constIcon(Icons.star,color:Colors.amber),'issue'=>constIcon(Icons.error_outline,color:Colors.green),'pull'=>constIcon(Icons.call_split,color:Colors.blue),'mention'=>constIcon(Icons.alternate_email,color:Colors.purple),'assigned'=>constIcon(Icons.assignment_ind,color:Colors.orange),_=>constIcon(Icons.notifications,color:Colors.grey),};}}

通知的交互设计

点击通知后的导航逻辑:

void_handleNotificationTap(NotificationItemnotification){// 标记为已读context.read<NotificationProvider>().markAsRead(notification.id);// 根据类型跳转switch(notification.type){case'star':case'forked':// 跳转到仓库详情finalparts=notification.repoFullName.split('/');Navigator.pushNamed(context,'/repo',arguments:{'owner':parts[0],'name':parts[1],});break;case'issue':case'mention':case'assigned':// 跳转到 Issue 详情Navigator.pushNamed(context,'/repo/issues/detail',arguments:_extractIssueArgs(notification));break;case'pullRequest':// 跳转到 PR 详情Navigator.pushNamed(context,'/repo/pulls/detail',arguments:_extractIssueArgs(notification));break;}}

性能考量

通知系统需要处理的性能问题:

未读计数。不应每次切换 Tab 都发起 API 请求。未读计数缓存在NotificationProvider._unreadCount中,通过轮询更新。Tab 切换时的 UI 更新是纯本地操作(从内存读取)。

列表分页。通知列表支持无限滚动,每页 30 条。首次加载只获取最新一页。用户向下滚动时按需加载历史通知。

后台同步。应用在后台时不进行网络请求。用户回到前台时,立即触发一次未读计数更新。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询