当前位置: 首页 > news >正文

AtomGit Flutter鸿蒙客户端:共享组件

组件库概览

项目中提取了 6 个可复用组件,位于lib/shared/widgets/lib/features/repo/widgets/

组件路径用途
ErrorRetryWidgetshared/widgets/错误状态 + 重试
LoadingIndicatorshared/widgets/加载中状态
MarkdownViewershared/widgets/Markdown 渲染
PaginatedListshared/widgets/无限滚动列表
UserAvatarshared/widgets/用户头像
RepoCardrepo/widgets/仓库卡片

ErrorRetryWidget

classErrorRetryWidgetextendsStatelessWidget{finalStringmessage;finalVoidCallback?onRetry;constErrorRetryWidget({super.key,requiredthis.message,this.onRetry,});@overrideWidgetbuild(BuildContextcontext){returnCenter(child:Padding(padding:constEdgeInsets.all(32),child:Column(mainAxisAlignment:MainAxisAlignment.center,children:[Icon(Icons.error_outline,size:64,color:Theme.of(context).colorScheme.error),constSizedBox(height:16),Text(message,textAlign:TextAlign.center,style:Theme.of(context).textTheme.bodyLarge),if(onRetry!=null)...[constSizedBox(height:24),FilledButton.icon(onPressed:onRetry,icon:constIcon(Icons.refresh),label:constText('重试'),),],],),),);}}

onRetry为可空参数 —— 提供时展示重试按钮,不提供时只展示错误信息。错误图标使用主题色colorScheme.error

典型用法:

if(provider.error!=null&&provider.repositories.isEmpty){returnErrorRetryWidget(message:provider.error!,onRetry:()=>provider.load(),);}

LoadingIndicator

classLoadingIndicatorextendsStatelessWidget{finalString?message;constLoadingIndicator({super.key,this.message});@overrideWidgetbuild(BuildContextcontext){returnCenter(child:Column(mainAxisAlignment:MainAxisAlignment.center,children:[constCircularProgressIndicator(),if(message!=null)...[constSizedBox(height:16),Text(message!,style:Theme.of(context).textTheme.bodyMedium),],],),);}}

简洁的居中加载指示器,可选文案。

MarkdownViewer

classMarkdownViewerextendsStatelessWidget{finalStringmarkdown;constMarkdownViewer({super.key,requiredthis.markdown});@overrideWidgetbuild(BuildContextcontext){returnMarkdownBody(data:markdown,styleSheet:MarkdownStyleSheet.fromTheme(Theme.of(context)).copyWith(h1:Theme.of(context).textTheme.headlineSmall,h2:Theme.of(context).textTheme.titleLarge,h3:Theme.of(context).textTheme.titleMedium,p:Theme.of(context).textTheme.bodyMedium,code:TextStyle(fontFamily:'monospace',fontSize:13,backgroundColor:Theme.of(context).colorScheme.surfaceContainerHighest,),codeblockDecoration:BoxDecoration(color:Theme.of(context).colorScheme.surfaceContainerHighest,borderRadius:BorderRadius.circular(8),),),);}}

包装flutter_markdownMarkdownBody,通过MarkdownStyleSheet.fromTheme()继承当前主题,再覆盖代码块和内联代码的样式。代码使用等宽字体和圆角背景容器。

PaginatedList

classPaginatedListextendsStatelessWidget{finalint itemCount;finalWidgetFunction(BuildContext,int)itemBuilder;finalVoidCallback?onLoadMore;finalbool isLoading;finalbool hasMore;finalStringemptyMessage;@overrideWidgetbuild(BuildContextcontext){if(itemCount==0&&!isLoading){returnCenter(child:Text(emptyMessage));}returnListView.builder(addAutomaticKeepAlives:true,itemCount:itemCount+(hasMore?1:0),itemBuilder:(context,index){if(index>=itemCount){returnconstPadding(padding:EdgeInsets.all(16),child:Center(child:CircularProgressIndicator()),);}returnitemBuilder(context,index);},);}}

addAutomaticKeepAlives: true保持列表项状态。当hasMore时在末尾追加加载指示器。空数据时展示emptyMessage

UserAvatar

classUserAvatarextendsStatelessWidget{finalString?avatarUrl;finaldouble size;finalString?name;constUserAvatar({super.key,this.avatarUrl,this.size=40,this.name,});@overrideWidgetbuild(BuildContextcontext){if(avatarUrl!=null&&avatarUrl!.isNotEmpty){returnCircleAvatar(radius:size/2,backgroundImage:NetworkImage(avatarUrl!),);}returnCircleAvatar(radius:size/2,backgroundColor:Theme.of(context).colorScheme.primaryContainer,child:Text((name?.isNotEmpty==true)?name![0].toUpperCase():'?',style:TextStyle(fontSize:size*0.4,color:Theme.of(context).colorScheme.onPrimaryContainer,),),);}}

有头像 URL 时加载网络图片,无 URL 时显示首字母大写或?。字体大小随size等比缩放。

RepoCard

最复杂的共享组件,包含完整的仓库信息展示:

classRepoCardextendsStatelessWidget{finalRepositoryrepo;finalVoidCallback?onTap;@overrideWidgetbuild(BuildContextcontext){returnCard(margin:constEdgeInsets.symmetric(horizontal:16,vertical:6),child:InkWell(onTap:onTap,borderRadius:BorderRadius.circular(12),child:Padding(padding:constEdgeInsets.all(16),child:Column(crossAxisAlignment:CrossAxisAlignment.start,children:[_buildHeader(context),if(repo.description!=null)...[constSizedBox(height:8),_buildDescription(context),],constSizedBox(height:12),_buildStats(context),],),),),);}}

头部:私有/公开图标 + 仓库名

Widget_buildHeader(BuildContextcontext){returnRow(children:[_buildPrivacyIcon(),constSizedBox(width:8),Expanded(child:Text(repo.fullName,style:Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight:FontWeight.w600,)),),]);}Widget_buildPrivacyIcon(){if(repo.isPrivate){returnImage.asset('assets/images/private.png',width:16,height:16,errorBuilder:(_,__,___)=>constIcon(Icons.lock_outline,size:16),);}returnconstIcon(Icons.book_outlined,size:16);}

尝试加载私有仓库的本地图标,失败时 fallback 到 Material 图标。

描述(最多 2 行)

Widget_buildDescription(BuildContextcontext){returnText(repo.description!,maxLines:2,overflow:TextOverflow.ellipsis,style:Theme.of(context).textTheme.bodyMedium);}

统计行

Widget_buildStats(BuildContextcontext){returnRow(children:[_LanguageDot(language:repo.language),if(repo.language!=null)...[constSizedBox(width:4),Text(repo.language!,style:Theme.of(context).textTheme.bodySmall),constSizedBox(width:16),],Icon(Icons.star_border,size:16,color:Theme.of(context).colorScheme.onSurfaceVariant),constSizedBox(width:2),Text(_formatCount(repo.stargazersCount),style:Theme.of(context).textTheme.bodySmall),constSizedBox(width:12),Icon(Icons.call_split,size:16,color:Theme.of(context).colorScheme.onSurfaceVariant),constSizedBox(width:2),Text(_formatCount(repo.forksCount),style:Theme.of(context).textTheme.bodySmall),constSpacer(),Text(DateFormatter.relative(repo.updatedAt),style:Theme.of(context).textTheme.bodySmall),]);}

语言颜色映射

final_languageColors={'Dart':constColor(0xFF00B4AB),'Python':constColor(0xFF3572A5),'JavaScript':constColor(0xFFF7DF1E),'TypeScript':constColor(0xFF3178C6),'Java':constColor(0xFFB07219),'Go':constColor(0xFF00ADD8),'Rust':constColor(0xFFDEA584),'C++':constColor(0xFFF34B7D),'C':constColor(0xFF555555),'Swift':constColor(0xFFF05138),'Kotlin':constColor(0xFFA97BFF),};Widget_languageDot(String?language){finalcolor=_languageColors[language]??Colors.grey;returnContainer(width:12,height:12,decoration:BoxDecoration(color:color,shape:BoxShape.circle,),);}

11 种语言的 GitHub 风格配色,未知语言默认为灰色。

数量格式化

String_formatCount(int count){if(count>=1000){return'${(count/1000).toStringAsFixed(1)}k';}returncount.toString();}

设计原则

  1. 无业务依赖—— 除 RepoCard 外,其余组件都是纯 UI,不依赖任何业务 Provider
  2. 可空回调——onTaponRetrymessage等都可空,按需提供
  3. 主题感知—— 所有颜色通过Theme.of(context)获取,支持深色模式
  4. 最小状态—— 全部使用 StatelessWidget,状态由父组件管理
http://www.cnnetsun.cn/news/2858822.html

相关文章:

  • 2026 年网站建设公司排行,综合实力盘点
  • 荣耀加冕!云智慧Cloudwise入选「2026 Global AI 100」榜单,彰显全球化商业硬实力
  • 使用k8s安装Sonarqube
  • 微微信朋友圈广告投放开户,朋友圈广告投放流程、收费标准详解,全国可投
  • 微信小程序计算机毕设之基于spring boot的校园二手交易平台系统小程序基于微信小程序校园二手交易平台系统小程序(完整前后端代码+说明文档+LW,调试定制等)
  • 基于知识图谱(Neo4j)和大语言模型(LLM)的图检索增强(GraphRAG)的数控车床主轴系统故障诊断智能问答系统
  • # Claude Code + Playwright MCP 使用
  • LaTeX 绘图单位避坑指南:为什么你的 Draw.io / Matplotlib 导入后字体总是不对?
  • Granite Time Series TTM R2 时间序列大模型交通流量预测实战教学
  • 【新版 SeaTunnel Web 最佳实践9】:11 个场景讲清楚 MySQL 到 Oracle 单表同步
  • 【技术干货】深度解析 Frontier Code: AI 代码生成的新基准与可合并性评测
  • Django 框架 深度学习
  • 匠心智造赋能发酵产业 信安诺亮相 2026 第 17 届杭州生物发酵展
  • OpenClaw连接使用chrome浏览器共享缓存cookie的方法
  • 公众号无限回调系统二开实战:破除域名限制的商用 PHP 方案
  • 百度内部启动青木、风雷两大计划,百度大动作该咋看?
  • 从唐诗到商品推荐:我用Neo4j Desktop给电商数据做了个“知识图谱”实验
  • 别再只会用插值了!用PyTorch的PixelShuffle给图像超分换个思路(附代码示例)
  • STM32H7超频到480MHz?聊聊时钟配置里的那些“潜规则”与稳定性测试
  • 告别“啥啥啥”:快速上手Xilinx MMCM原语,搞定多路时钟生成与相位调整
  • 保姆级教程:手把手教你从零写一个Rimworld 1.4 Mod的About.xml配置文件
  • 别再只用默认值了!深入解读达梦DM8的V$CIPHERS加密算法视图
  • 文本任务评估指标选择指南:匹配、生成、排序三类问题的正确解法
  • GPT-4的1.8万亿参数与2%激活率:硬件代价与工程真相
  • STM32项目实战:用NRF24L01+和HAL库DIY一个简易无线遥控器(带按键和LED反馈)
  • 别再让雷劈坏你的设备了!手把手教你为RS485接口选配TVS、GDT和TBU(附IEC标准解读)
  • 当自监督学习遇上OoD检测:不用人工标注,用CSI和SSD算法发现数据中的‘未知数’
  • 别再为PDF乱码发愁!Elsevier投稿时LaTeX的.cls文件保姆级获取指南
  • 警惕技术术语虚构:MCP并非真实存在的LLM通信协议
  • 用Python的tifffile库搞定病理大图:从生成带金字塔的OME-TIFF到用QuPath流畅查看