一、阶段目标与平台背景

1.1 项目迭代背景

本项目基于昨天开发的 AtomGit 口袋工具,在第一阶段成功实现了基础的搜索功能和仓库列表展示,并打通了与 AtomGit OpenAPI 的数据交互。随着核心功能的初步完成,第二阶段我们将聚焦于用户体验优化与代码组件化设计,旨在构建一个更稳定、可维护且用户友好的应用程序。

在初步版本中,虽然功能已基本可用,但存在以下可优化点:

  1. 界面交互较为基础,缺乏流畅的刷新体验
  2. 重复的布局代码增加了维护成本
  3. 数据加载和错误处理机制有待完善

本阶段将围绕这些问题展开改进,为后续的功能扩展打下坚实基础。

1.2 AtomGit API

AtomGit 作为国内知名的代码托管平台,其 API 提供了丰富的接口能力。

二、核心资料与技术栈

2.1 第三方库与依赖

在本次迭代中,我们在 pubspec.yaml 中新增刷新库依赖:

dependencies:
  http: ^1.2.2
  pull_to_refresh_flutter3: ^2.0.2

pull_to_refresh_flutter3 提供 SmartRefresher 组件以及 RefreshController,可快速实现下拉刷新、上拉加载与多种状态提示。还是挺方便挺好用的。

2.3 数据模型与封装

  • Repository:仓库数据模型,封装 id、名称、描述、语言、Star/Fork 数、更新时间等字段,并提供 formattedUpdatedAtdisplayName 等派生属性。
class Repository {
  final int id;
  final String name;
  final String fullName;
  final String? description;
  final String? htmlUrl;
  final String? language;
  final int stargazersCount;
  final int forksCount;
  final String? ownerName;
  final String? ownerAvatarUrl;
  final DateTime? updatedAt;
  final String? namespacePath;  // 新增:namespace的path,用于API调用

  Repository({
    required this.id,
    required this.name,
    required this.fullName,
    required this.description,
    required this.htmlUrl,
    required this.language,
    required this.stargazersCount,
    required this.forksCount,
    required this.ownerName,
    required this.ownerAvatarUrl,
    required this.updatedAt,
    this.namespacePath,
  });

  factory Repository.fromJson(Map<String, dynamic> json) {
    final owner = json['owner'];
    final namespace = json['namespace'];
    String? ownerName;
    String? ownerAvatarUrl;
    String? namespacePath;

    // 优先从namespace获取真实的仓库所有者信息
    if (namespace is Map<String, dynamic>) {
      ownerName = namespace['path']?.toString() ?? namespace['name']?.toString();
      ownerAvatarUrl = namespace['avatar']?.toString();
      namespacePath = namespace['path']?.toString();
    }
    
    // 如果namespace没有,则从owner获取
    if (ownerName == null && owner is Map<String, dynamic>) {
      ownerName = owner['login']?.toString() ?? owner['name']?.toString();
      ownerAvatarUrl = owner['avatar_url']?.toString();
    } else if (ownerName == null && owner is String) {
      ownerName = owner;
      ownerAvatarUrl = json['avatar_url']?.toString();
    }

    // 使用 path 字段作为仓库名(这是 API 调用需要的),而不是 name(可能是中文显示名)
    final fullName = json['full_name']?.toString() ?? json['path']?.toString() ?? json['name']?.toString() ?? '';
    // path 是仓库的实际路径名,name 可能是显示名(如中文名)
    final name = json['path']?.toString() ?? json['name']?.toString() ?? '';

    return Repository(
      id: _parseInt(json['id']),
      name: name,
      fullName: fullName,
      description: json['description']?.toString(),
      htmlUrl: json['html_url']?.toString(),
      language: json['language']?.toString(),
      stargazersCount: _parseInt(json['stargazers_count']),
      forksCount: _parseInt(json['forks_count']),
      ownerName: ownerName,
      ownerAvatarUrl: ownerAvatarUrl,
      updatedAt: _parseDateTime(json['updated_at']),
      namespacePath: namespacePath,
    );
  }

  static int _parseInt(dynamic value) {
    if (value is int) return value;
    if (value is String) {
      return int.tryParse(value) ?? 0;
    }
    return 0;
  }

  static DateTime? _parseDateTime(dynamic value) {
    if (value == null) return null;
    final text = value.toString();
    try {
      return DateTime.parse(text).toLocal();
    } catch (_) {
      return null;
    }
  }

  String get displayName => name.isNotEmpty ? name : fullName;

  String get formattedUpdatedAt {
    if (updatedAt == null) return '未知';
    final date = updatedAt!;
    return '${date.year}-${_twoDigits(date.month)}-${_twoDigits(date.day)} ${_twoDigits(date.hour)}:${_twoDigits(date.minute)}';
    }

  static String _twoDigits(int value) => value.toString().padLeft(2, '0');
}

repository

  • UserSummary:搜索用户列表项结构,包含 id、login、avatar、profile 链接等。
const UserSummary({
    required this.id,
    required this.login,
    required this.htmlUrl,
    required this.avatarUrl,
    required this.type,
    required this.score,
  });

UserSummary

三、实现步骤详解

3.1 API 服务层升级

lib/services/api_service.dart 中统一封装 GET 请求,添加公共 Header,提升代码复用性:

static Map<String, String> get _headers => <String, String>{
  'Authorization': 'Bearer $token',
  'Accept': 'application/json',
};

static Future<http.Response> _get(Uri url) {
  return http.get(url, headers: _headers);
}

针对不同接口分别返回模型:

  • searchUsers:支持 page / per_page,根据返回结构构造 UserSearchResult
  • getUserRepositories:返回 List<Repository>,用于“我的仓库”分页。

通过 Future + try/catch 引导上层处理异常,错误信息携带状态码与响应体便于定位问题。

3.2 自定义仓库卡片组件

lib/widgets/repository_card.dart 负责统一仓库信息的展示:

  • 头部采用 CircleAvatar 展示仓库所有者头像,若缺失则显示首字母;
  • 主体呈现仓库名称、作者、描述;
  • 尾部通过 Chip 集合展示 Star 数、Fork 数、语言和更新时间。

该组件可在搜索结果和“我的仓库”页面复用,避免重复布局代码,可以为后续的开发提供便利。

四、运行结果

运行结果

以上是运行的结果,仓库显示出来的卡片已经组件化。也包含了仓库名称,编程语言等
这次组件化之后,会使得后续的开发更加的方便。明天继续加油

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐