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

组件库概览
项目中提取了 6 个可复用组件,位于 lib/shared/widgets/ 和 lib/features/repo/widgets/:
| 组件 | 路径 | 用途 |
|---|---|---|
ErrorRetryWidget |
shared/widgets/ |
错误状态 + 重试 |
LoadingIndicator |
shared/widgets/ |
加载中状态 |
MarkdownViewer |
shared/widgets/ |
Markdown 渲染 |
PaginatedList |
shared/widgets/ |
无限滚动列表 |
UserAvatar |
shared/widgets/ |
用户头像 |
RepoCard |
repo/widgets/ |
仓库卡片 |
ErrorRetryWidget
class ErrorRetryWidget extends StatelessWidget {
final String message;
final VoidCallback? onRetry;
const ErrorRetryWidget({
super.key,
required this.message,
this.onRetry,
});
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: const EdgeInsets.all(32),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.error_outline, size: 64,
color: Theme.of(context).colorScheme.error),
const SizedBox(height: 16),
Text(message,
textAlign: TextAlign.center,
style: Theme.of(context).textTheme.bodyLarge),
if (onRetry != null) ...[
const SizedBox(height: 24),
FilledButton.icon(
onPressed: onRetry,
icon: const Icon(Icons.refresh),
label: const Text('重试'),
),
],
],
),
),
);
}
}
onRetry 为可空参数 —— 提供时展示重试按钮,不提供时只展示错误信息。错误图标使用主题色 colorScheme.error。
典型用法:
if (provider.error != null && provider.repositories.isEmpty) {
return ErrorRetryWidget(
message: provider.error!,
onRetry: () => provider.load(),
);
}
LoadingIndicator
class LoadingIndicator extends StatelessWidget {
final String? message;
const LoadingIndicator({super.key, this.message});
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
if (message != null) ...[
const SizedBox(height: 16),
Text(message!,
style: Theme.of(context).textTheme.bodyMedium),
],
],
),
);
}
}
简洁的居中加载指示器,可选文案。
MarkdownViewer
class MarkdownViewer extends StatelessWidget {
final String markdown;
const MarkdownViewer({super.key, required this.markdown});
Widget build(BuildContext context) {
return MarkdownBody(
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_markdown 的 MarkdownBody,通过 MarkdownStyleSheet.fromTheme() 继承当前主题,再覆盖代码块和内联代码的样式。代码使用等宽字体和圆角背景容器。
PaginatedList
class PaginatedList extends StatelessWidget {
final int itemCount;
final Widget Function(BuildContext, int) itemBuilder;
final VoidCallback? onLoadMore;
final bool isLoading;
final bool hasMore;
final String emptyMessage;
Widget build(BuildContext context) {
if (itemCount == 0 && !isLoading) {
return Center(child: Text(emptyMessage));
}
return ListView.builder(
addAutomaticKeepAlives: true,
itemCount: itemCount + (hasMore ? 1 : 0),
itemBuilder: (context, index) {
if (index >= itemCount) {
return const Padding(
padding: EdgeInsets.all(16),
child: Center(child: CircularProgressIndicator()),
);
}
return itemBuilder(context, index);
},
);
}
}
addAutomaticKeepAlives: true 保持列表项状态。当 hasMore 时在末尾追加加载指示器。空数据时展示 emptyMessage。
UserAvatar
class UserAvatar extends StatelessWidget {
final String? avatarUrl;
final double size;
final String? name;
const UserAvatar({
super.key,
this.avatarUrl,
this.size = 40,
this.name,
});
Widget build(BuildContext context) {
if (avatarUrl != null && avatarUrl!.isNotEmpty) {
return CircleAvatar(
radius: size / 2,
backgroundImage: NetworkImage(avatarUrl!),
);
}
return CircleAvatar(
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
最复杂的共享组件,包含完整的仓库信息展示:
class RepoCard extends StatelessWidget {
final Repository repo;
final VoidCallback? onTap;
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 6),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(context),
if (repo.description != null) ...[
const SizedBox(height: 8),
_buildDescription(context),
],
const SizedBox(height: 12),
_buildStats(context),
],
),
),
),
);
}
}
头部:私有/公开图标 + 仓库名
Widget _buildHeader(BuildContext context) {
return Row(children: [
_buildPrivacyIcon(),
const SizedBox(width: 8),
Expanded(
child: Text(repo.fullName,
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
)),
),
]);
}
Widget _buildPrivacyIcon() {
if (repo.isPrivate) {
return Image.asset(
'assets/images/private.png',
width: 16,
height: 16,
errorBuilder: (_, __, ___) =>
const Icon(Icons.lock_outline, size: 16),
);
}
return const Icon(Icons.book_outlined, size: 16);
}
尝试加载私有仓库的本地图标,失败时 fallback 到 Material 图标。
描述(最多 2 行)
Widget _buildDescription(BuildContext context) {
return Text(repo.description!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.bodyMedium);
}
统计行
Widget _buildStats(BuildContext context) {
return Row(children: [
_LanguageDot(language: repo.language),
if (repo.language != null) ...[
const SizedBox(width: 4),
Text(repo.language!, style: Theme.of(context).textTheme.bodySmall),
const SizedBox(width: 16),
],
Icon(Icons.star_border, size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant),
const SizedBox(width: 2),
Text(_formatCount(repo.stargazersCount),
style: Theme.of(context).textTheme.bodySmall),
const SizedBox(width: 12),
Icon(Icons.call_split, size: 16,
color: Theme.of(context).colorScheme.onSurfaceVariant),
const SizedBox(width: 2),
Text(_formatCount(repo.forksCount),
style: Theme.of(context).textTheme.bodySmall),
const Spacer(),
Text(DateFormatter.relative(repo.updatedAt),
style: Theme.of(context).textTheme.bodySmall),
]);
}
语言颜色映射
final _languageColors = {
'Dart': const Color(0xFF00B4AB),
'Python': const Color(0xFF3572A5),
'JavaScript': const Color(0xFFF7DF1E),
'TypeScript': const Color(0xFF3178C6),
'Java': const Color(0xFFB07219),
'Go': const Color(0xFF00ADD8),
'Rust': const Color(0xFFDEA584),
'C++': const Color(0xFFF34B7D),
'C': const Color(0xFF555555),
'Swift': const Color(0xFFF05138),
'Kotlin': const Color(0xFFA97BFF),
};
Widget _languageDot(String? language) {
final color = _languageColors[language] ?? Colors.grey;
return Container(
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';
}
return count.toString();
}
设计原则
- 无业务依赖 —— 除 RepoCard 外,其余组件都是纯 UI,不依赖任何业务 Provider
- 可空回调 ——
onTap、onRetry、message等都可空,按需提供 - 主题感知 —— 所有颜色通过
Theme.of(context)获取,支持深色模式 - 最小状态 —— 全部使用 StatelessWidget,状态由父组件管理
更多推荐

所有评论(0)