【2025版 OpenHarmony】GitCode 口袋工具 v1.0.1 更新发布:Flutter + HarmonyOS 封装导航栏进行跳转

GitCode 口袋工具 v1.0.1 更新发布:Flutter 跨平台搜索助手的完整实现

📝 前言

今天很高兴为大家分享 GitCode 口袋工具 的最新版本(v1.0.1)。这是一个基于 Flutter 开发的轻量级 GitCode 搜索工具,支持搜索用户和仓库,提供流畅的分页加载体验。项目专为 OpenHarmony 平台优化。

本文将详细介绍项目的核心功能、技术实现和代码架构,希望能为 Flutter 开发者提供一些参考和启发。

✨ 项目概述

GitCode 口袋工具是一个跨平台的 GitCode 搜索客户端,主要功能包括:

  • 🔍 用户搜索:快速搜索 GitCode 用户,查看用户详细信息
  • 📦 仓库搜索:搜索代码仓库,查看仓库详情和统计信息
  • 📱 分页加载:支持下拉刷新和上拉加载更多
  • 🎨 Material Design 3:现代化的 UI 设计
  • 🪟 HarmonyOS平台支持:支持 HarmonyOS

🛠️ 技术栈

核心框架

  • Flutter 3.6.2+:跨平台 UI 框架
  • Dart 3.6.2+:编程语言

主要依赖

  • dio ^5.7.0:强大的 HTTP 网络请求库
  • pull_to_refresh ^2.0.0:下拉刷新和上拉加载更多组件
  • go_router ^14.6.2:声明式路由管理库(已集成,为后续功能扩展准备)
  • url_launcher ^6.3.1:用于打开外部链接

🏗️ 项目架构

项目采用清晰的分层架构,主要分为以下几个模块:

复制代码
lib/
├── core/                           # 核心功能模块
│   ├── app_config.dart            # 应用配置(Token 等常量)
│   └── gitcode_api.dart           # GitCode API 客户端封装
│
├── pages/                          # 页面模块
│   ├── main_navigation/           # 主导航相关页面
│   │   ├── intro_page.dart        # 首页/介绍页
│   │   ├── search_page.dart       # 搜索页面
│   │   └── profile_page.dart      # 我的/个人信息页
│   ├── repository_list_page.dart  # 仓库列表页(分页加载)
│   └── user_list_page.dart        # 用户列表页(分页加载)
│
├── widgets/                        # 自定义组件
│   ├── repository_card.dart       # 仓库信息卡片组件
│   └── user_card.dart             # 用户信息卡片组件
│
└── main.dart                       # 应用入口,包含主应用和导航栏

💻 核心代码实现

1. API 客户端封装

项目核心是 GitCodeApiClient 类,它封装了所有与 GitCode API 的交互逻辑。

1.1 API 客户端初始化
dart 复制代码
class GitCodeApiClient {
  GitCodeApiClient({Dio? dio})
      : _dio = dio ??
            Dio(
              BaseOptions(
                baseUrl: 'https://api.gitcode.com/api/v5',
                // 连接和读取阶段都限定为 5 秒,能在弱网场景下快速失败
                connectTimeout: const Duration(seconds: 5),
                receiveTimeout: const Duration(seconds: 5),
              ),
            );

  final Dio _dio;
  // ...
}

关键特性:

  • 统一配置 API 基础地址
  • 设置合理的超时时间(5秒),快速失败机制
  • 支持依赖注入,便于测试
1.2 用户搜索实现
dart 复制代码
Future<List<GitCodeSearchUser>> searchUsers({
  required String keyword,
  required String personalToken,
  int perPage = 10,
  int page = 1,
}) async {
  final trimmed = keyword.trim();
  if (trimmed.isEmpty) {
    throw const GitCodeApiException('请输入搜索关键字');
  }

  final response = await _dio.get<List<dynamic>>(
    '/search/users',
    queryParameters: <String, dynamic>{
      'q': trimmed,
      'access_token': personalToken,
      // clamp 可以阻止业务层传入不合法的分页参数
      'per_page': perPage.clamp(1, 50),
      'page': page.clamp(1, 100),
    },
    options: Options(
      headers: _buildHeaders(personalToken),
      responseType: ResponseType.json,
      validateStatus: (status) => status != null && status < 500,
    ),
  );

  final statusCode = response.statusCode ?? 0;
  if (statusCode == 401) {
    throw const GitCodeApiException('Token 无效或权限不足,无法搜索用户');
  }
  if (statusCode != 200 || response.data == null) {
    throw GitCodeApiException('搜索用户失败 (HTTP $statusCode)');
  }

  return response.data!
      .whereType<Map<String, dynamic>>()
      .map(GitCodeSearchUser.fromJson)
      .toList();
}

实现亮点:

  • 输入验证:自动去除前后空格,防止无效请求
  • 参数校验:使用 clamp 方法限制分页参数范围(1-50,1-100)
  • 错误处理:针对 401 未授权错误提供友好提示
  • 数据转换:安全的 JSON 解析,使用类型检查和空值处理
1.3 智能用户查找(昵称映射)

这是一个很有特色的功能,当用户输入昵称而非登录名时,系统会自动通过搜索接口映射到真实登录名:

dart 复制代码
Future<GitCodeUser> fetchUser(
  String username, {
  String? personalToken,
  bool allowSearchFallback = true,
}) async {
  final trimmed = username.trim();
  // ... 基本验证

  try {
    final response = await _dio.get<Map<String, dynamic>>(
      '/users/${Uri.encodeComponent(trimmed)}',
      // ... 请求配置
    );

    final statusCode = response.statusCode ?? 0;
    switch (statusCode) {
      case 200:
        // 成功返回用户信息
        return GitCodeUser.fromJson(data);
      case 404:
        // 如果允许降级且提供了 Token,尝试通过搜索接口映射
        if (allowSearchFallback &&
            personalToken != null &&
            personalToken.isNotEmpty) {
          final fallbackLogin = await _searchLoginByKeyword(
            trimmed,
            personalToken: personalToken,
          );
          if (fallbackLogin != null && fallbackLogin != trimmed) {
            // 使用真实登录名重新查询,关闭二次降级避免递归
            return fetchUser(
              fallbackLogin,
              personalToken: personalToken,
              allowSearchFallback: false,
            );
          }
        }
        throw GitCodeApiException(
          '未找到用户 $trimmed,若输入的是展示昵称,请改用登录名或携带 token 搜索。',
        );
      // ... 其他错误处理
    }
  } catch (error) {
    // ... 异常处理
  }
}

设计思路:

  • 提升用户体验:用户不需要知道确切的登录名
  • 防止递归:使用 allowSearchFallback 标志避免无限递归
  • 优雅降级:API 直接查询失败时,自动尝试搜索接口

2. 分页加载实现

项目使用 pull_to_refresh 包实现流畅的分页加载体验。

2.1 用户列表页实现
dart 复制代码
class _UserListPageState extends State<UserListPage> {
  final RefreshController _refreshController = RefreshController(
    initialRefresh: false,
  );
  final GitCodeApiClient _client = GitCodeApiClient();

  List<GitCodeSearchUser> _users = [];
  int _currentPage = 1;
  final int _perPage = 20;
  bool _hasMore = true;
  String? _errorMessage;
  bool _isLoading = false;

  @override
  void initState() {
    super.initState();
    _loadUsers(refresh: true);
  }

  /// 加载用户列表
  Future<void> _loadUsers({bool refresh = false}) async {
    if (_isLoading) return;

    if (refresh) {
      _currentPage = 1;
      _hasMore = true;
      _users.clear();
    }

    if (!_hasMore) {
      _refreshController.loadNoData();
      return;
    }

    setState(() {
      _isLoading = true;
      _errorMessage = null;
    });

    try {
      final users = await _client.searchUsers(
        keyword: widget.keyword,
        personalToken: widget.token,
        perPage: _perPage,
        page: _currentPage,
      );

      setState(() {
        if (refresh) {
          _users = users;
        } else {
          _users.addAll(users);
        }

        // 判断是否还有更多数据
        _hasMore = users.length >= _perPage;
        _currentPage++;
        _errorMessage = null;
        _isLoading = false;
      });

      // 更新刷新控制器状态
      if (refresh) {
        _refreshController.refreshCompleted();
      } else {
        if (_hasMore) {
          _refreshController.loadComplete();
        } else {
          _refreshController.loadNoData();
        }
      }
    } on GitCodeApiException catch (e) {
      setState(() {
        _errorMessage = e.message;
        _isLoading = false;
      });
      if (refresh) {
        _refreshController.refreshFailed();
      } else {
        _refreshController.loadFailed();
      }
    }
  }

  /// 下拉刷新
  void _onRefresh() {
    _loadUsers(refresh: true);
  }

  /// 上拉加载
  void _onLoading() {
    _loadUsers(refresh: false);
  }
}

核心逻辑:

  • 状态管理 :使用 _currentPage_hasMore_isLoading 等状态变量管理加载状态
  • 数据合并:刷新时替换数据,加载更多时追加数据
  • 智能判断:根据返回数据量判断是否还有更多数据
  • 错误处理:完善的异常捕获和用户提示
2.2 UI 渲染
dart 复制代码
SmartRefresher(
  controller: _refreshController,
  enablePullDown: true,
  enablePullUp: _hasMore,
  header: const ClassicHeader(
    refreshingText: '刷新中...',
    completeText: '刷新完成',
    idleText: '下拉刷新',
    releaseText: '释放刷新',
  ),
  footer: ClassicFooter(
    loadingText: '加载中...',
    noDataText: '没有更多数据了',
    idleText: '上拉加载更多',
    canLoadingText: '释放加载',
  ),
  onRefresh: _onRefresh,
  onLoading: _onLoading,
  child: _users.isEmpty && _isLoading
      ? const Center(child: CircularProgressIndicator())
      : _users.isEmpty
          ? Center(
              child: Column(
                mainAxisAlignment: MainAxisAlignment.center,
                children: [
                  Icon(Icons.search_off, size: 64, color: Colors.grey[400]),
                  const SizedBox(height: 16),
                  Text('暂无数据', style: TextStyle(color: Colors.grey[600])),
                ],
              ),
            )
          : ListView.builder(
              itemCount: _users.length,
              padding: const EdgeInsets.symmetric(vertical: 8),
              itemBuilder: (context, index) {
                return UserCard(
                  user: _users[index],
                  onTap: () {
                    // 处理点击事件
                  },
                );
              },
            ),
)

用户体验优化:

  • 加载状态:首次加载显示加载指示器
  • 空状态:数据为空时显示友好提示
  • 本地化文本:所有提示文本都是中文,符合用户习惯

3. 自定义组件实现

项目设计了两个精美的卡片组件用于展示用户和仓库信息。

3.1 用户卡片组件
dart 复制代码
class UserCard extends StatelessWidget {
  const UserCard({
    super.key,
    required this.user,
    this.onTap,
    this.height,
  });

  final GitCodeSearchUser user;
  final VoidCallback? onTap;
  final double? height;

  // 固定每行的高度常量
  static const double _avatarSize = 40.0;
  static const double _titleRowHeight = 20.0;
  static const double _loginRowHeight = 18.0;
  static const double _infoRowHeight = 18.0;
  static const double _urlRowHeight = 18.0;
  static const double _spacing = 6.0;
  static const double _padding = 12.0;

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);
    // 计算实际需要的高度,防止溢出
    final calculatedHeight = _padding * 2 +
        _avatarSize +
        _spacing +
        _titleRowHeight +
        _spacing +
        _loginRowHeight +
        _spacing +
        _infoRowHeight +
        _spacing +
        _urlRowHeight +
        8.0; // 额外余量防止溢出
    final cardHeight = height ?? calculatedHeight;

    final displayName = user.name?.isNotEmpty == true ? user.name! : user.login;

    return SizedBox(
      height: cardHeight,
      child: Card(
        elevation: 2,
        margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(12),
        ),
        child: InkWell(
          onTap: onTap,
          borderRadius: BorderRadius.circular(12),
          child: Padding(
            padding: const EdgeInsets.all(_padding),
            child: Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                // 左侧:头像
                CircleAvatar(
                  radius: _avatarSize / 2,
                  backgroundImage: NetworkImage(user.avatarUrl),
                  onBackgroundImageError: (_, __) {},
                  backgroundColor: theme.colorScheme.surfaceVariant,
                  child: user.avatarUrl.isEmpty
                      ? Icon(
                          Icons.person,
                          size: _avatarSize / 2,
                          color: theme.colorScheme.onSurfaceVariant,
                        )
                      : null,
                ),
                const SizedBox(width: 12),
                // 右侧:用户信息
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    mainAxisSize: MainAxisSize.min,
                    children: [
                      // 用户名称(固定高度)
                      SizedBox(
                        height: _titleRowHeight,
                        child: Text(
                          displayName,
                          style: theme.textTheme.titleMedium?.copyWith(
                            fontWeight: FontWeight.bold,
                            fontSize: 16,
                          ),
                          maxLines: 1,
                          overflow: TextOverflow.ellipsis,
                        ),
                      ),
                      // ... 其他信息行
                    ],
                  ),
                ),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

设计亮点:

  • 固定高度:使用固定高度避免列表滚动时的布局抖动
  • 错误处理:头像加载失败时显示默认图标
  • 信息展示:清晰的层次结构,重要信息突出显示
  • 交互反馈 :使用 InkWell 提供点击波纹效果
3.2 仓库卡片组件
dart 复制代码
class RepositoryCard extends StatelessWidget {
  // ... 类似的结构

  Color _getLanguageColor(String language) {
    // 根据语言返回不同颜色
    final colors = <String, Color>{
      'Dart': Colors.blue,
      'Java': Colors.orange,
      'JavaScript': Colors.yellow[700]!,
      'Python': Colors.blue[300]!,
      'Go': Colors.cyan,
      'TypeScript': Colors.blue[800]!,
      'C++': Colors.pink,
      'C': Colors.grey[800]!,
      'Swift': Colors.orange[300]!,
      'Kotlin': Colors.purple,
      'Rust': Colors.orange[900]!,
    };
    return colors[language] ?? Colors.grey[600]!;
  }

  String _formatNumber(int number) {
    // 千位格式化,例如 1500 -> 1.5k
    if (number >= 1000) {
      return '${(number / 1000).toStringAsFixed(1)}k';
    }
    return number.toString();
  }
}

特色功能:

  • 语言颜色映射:不同编程语言使用不同颜色标识,提升视觉识别度
  • 数字格式化:大数字自动格式化为 k 单位(如 1.5k),提升可读性
  • 私有仓库标识:使用锁图标标识私有仓库

4. 搜索页面实现

搜索页面是应用的核心功能页面,支持用户和仓库两种搜索模式。

4.1 搜索状态管理
dart 复制代码
class _SearchPageState extends State<SearchPage> {
  final _keywordController = TextEditingController();
  final _tokenController = TextEditingController();
  final _formKey = GlobalKey<FormState>();
  final _client = GitCodeApiClient();

  QueryMode _mode = QueryMode.user;
  List<GitCodeSearchUser> _userResults = const [];
  List<GitCodeRepository> _repoResults = const [];
  String? _errorMessage;
  bool _isLoading = false;
  bool _obscureToken = true; // Token 显示/隐藏状态

  @override
  void dispose() {
    _keywordController.dispose();
    _tokenController.dispose();
    super.dispose();
  }
}

设计要点:

  • 表单验证 :使用 GlobalKey<FormState> 进行表单验证
  • 资源管理 :在 dispose 中释放 TextEditingController,防止内存泄漏
  • 状态隔离:用户和仓库搜索结果分开存储,避免跨模式数据混淆
4.2 统一搜索入口
dart 复制代码
Future<void> _performSearch() async {
  if (_isLoading) return;
  if (!_formKey.currentState!.validate()) return;

  FocusScope.of(context).unfocus(); // 收起键盘

  setState(() {
    _isLoading = true;
    _errorMessage = null;
    _userResults = const [];
    _repoResults = const [];
  });

  final token = _tokenController.text.trim();

  try {
    // 根据当前模式执行对应的 API 请求
    if (_mode == QueryMode.user) {
      final users = await _client.searchUsers(
        keyword: _keywordController.text,
        personalToken: token,
        perPage: 8,
      );
      setState(() {
        _userResults = users;
      });
    } else {
      final repos = await _client.searchRepositories(
        keyword: _keywordController.text,
        personalToken: token,
        perPage: 8,
      );
      setState(() {
        _repoResults = repos;
      });
    }
  } on GitCodeApiException catch (error) {
    setState(() {
      _errorMessage = error.message;
    });
  } catch (error) {
    setState(() {
      _errorMessage = '未知错误:$error';
    });
  } finally {
    if (mounted) {
      setState(() {
        _isLoading = false;
      });
    }
  }
}

用户体验优化:

  • 防止重复请求 :通过 _isLoading 标志防止并发请求
  • 表单验证:搜索前验证输入合法性
  • 自动收起键盘:搜索时自动收起键盘,提升体验
  • 错误处理:区分 API 异常和未知异常,提供相应提示
4.3 Token 安全输入
dart 复制代码
TextFormField(
  controller: _tokenController,
  decoration: InputDecoration(
    labelText: 'Access Token',
    hintText: 'GitCode 个人设置 > Access Token',
    prefixIcon: const Icon(Icons.vpn_key_outlined),
    suffixIcon: IconButton(
      icon: Icon(
        _obscureToken
            ? Icons.visibility_off_outlined
            : Icons.visibility_outlined,
      ),
      onPressed: () {
        setState(() {
          _obscureToken = !_obscureToken;
        });
      },
    ),
  ),
  obscureText: _obscureToken, // 支持显示/隐藏切换
  validator: (value) {
    if (value == null || value.trim().isEmpty) {
      return '搜索接口需提供 access token';
    }
    return null;
  },
),

安全特性:

  • 默认隐藏:Token 默认隐藏,保护敏感信息
  • 一键切换:点击眼睛图标快速切换显示/隐藏状态
  • 输入验证:表单验证确保 Token 不为空

5. 主导航实现

应用使用底部导航栏管理三个主要页面,使用 IndexedStack 保持页面状态。

dart 复制代码
class MainNavigationPage extends StatefulWidget {
  const MainNavigationPage({super.key});

  @override
  State<MainNavigationPage> createState() => _MainNavigationPageState();
}

class _MainNavigationPageState extends State<MainNavigationPage> {
  int _currentIndex = 0;

  final List<Widget> _pages = const [
    IntroPage(),
    SearchPage(),
    ProfilePage(),
  ];

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: IndexedStack(
        index: _currentIndex,
        children: _pages,
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _currentIndex,
        onDestinationSelected: (int index) {
          setState(() {
            _currentIndex = index;
          });
        },
        destinations: const [
          NavigationDestination(
            icon: Icon(Icons.home_outlined),
            selectedIcon: Icon(Icons.home),
            label: '首页',
          ),
          NavigationDestination(
            icon: Icon(Icons.search_outlined),
            selectedIcon: Icon(Icons.search),
            label: '搜索',
          ),
          NavigationDestination(
            icon: Icon(Icons.person_outline),
            selectedIcon: Icon(Icons.person),
            label: '我的',
          ),
        ],
      ),
    );
  }
}

设计优势:

  • 状态保持 :使用 IndexedStack 保持所有页面状态,切换时不重建页面
  • Material 3 :使用 NavigationBar 组件,符合 Material Design 3 规范
  • 图标切换:选中和未选中状态使用不同图标,视觉反馈清晰

🎯 关键技术点总结

1. 错误处理机制

项目实现了完善的错误处理机制:

  • 网络超时:连接和读取超时均为 5 秒,快速失败并提示用户
  • Token 验证:401 错误时提示 Token 无效或权限不足
  • 智能降级:用户搜索 404 时,自动尝试通过搜索接口映射昵称到登录名
  • 友好提示:所有错误信息都经过本地化处理,用户友好

2. 性能优化

  • 固定高度卡片:避免列表滚动时的布局抖动
  • IndexedStack:保持页面状态,避免重复构建
  • 懒加载 :使用 ListView.builder 实现列表项的懒加载
  • 请求去重:通过状态标志防止重复请求

3. 用户体验优化

  • 加载状态:所有异步操作都有明确的加载指示
  • 空状态处理:数据为空时显示友好提示
  • 输入验证:表单输入实时验证,防止无效请求
  • Token 安全:支持 Token 输入框显示/隐藏切换

📦 使用指南

快速开始

  1. 克隆项目
bash 复制代码
git clone https://gitcode.com/byyixuan/gitcode_pocket_tool.git
cd gitcode_pocket_tool
  1. 安装依赖
bash 复制代码
flutter pub get
  1. 配置 Access Token

lib/core/app_config.dart 文件中配置你的 GitCode Access Token:

dart 复制代码
static const demoToken = '<你的 GitCode 个人访问令牌>';
  1. 运行项目
bash 复制代码
# OpenHarmony 平台
flutter run

# Windows 平台
flutter run -d windows

获取 Access Token

  1. 登录 GitCode
  2. 进入 个人设置 > Access Token
  3. 创建新的访问令牌
  4. 复制令牌并粘贴到应用的 Token 输入框

🚀 后续计划

近期计划

  • 支持更多搜索过滤条件(语言、排序方式等)
  • 添加用户详情页面(展示完整用户信息)
  • 添加仓库详情页面(展示完整仓库信息)
  • 支持收藏功能(本地存储)
  • 支持搜索历史记录
  • 暗色模式支持

长期计划

  • 支持多语言国际化
  • 添加数据缓存机制
  • 支持离线浏览
  • 添加分享功能
  • 支持自定义主题

📝 总结

GitCode 口袋工具 v1.0.1 版本实现了一个功能完整、体验流畅的 GitCode 搜索客户端。项目采用了清晰的架构设计,完善的错误处理机制,以及优秀的用户体验优化。

核心亮点:

  1. ✅ 完善的 API 封装,支持智能降级和错误处理
  2. ✅ 流畅的分页加载体验,支持下拉刷新和上拉加载
  3. ✅ 精美的 UI 组件设计,固定高度避免布局抖动
  4. ✅ 完善的错误处理和用户提示
  5. ✅ 清晰的代码架构,易于维护和扩展

如果你对这个项目感兴趣,欢迎 Star 和 Fork!也欢迎提交 Issue 和 Pull Request,共同完善这个项目。

🔗 相关链接


⭐ 如果这个项目对你有帮助,请给个 Star 支持一下!

相关推荐
不羁的木木4 小时前
【开源鸿蒙跨平台开发学习笔记】Day01:React Native 开发 HarmonyOS-环境搭建篇
学习·开源·harmonyos
lqj_本人5 小时前
鸿蒙与Qt的双线程模型:主线程与UI线程的博弈
qt·ui·harmonyos
御承扬5 小时前
鸿蒙原生系列之拖拽事件
华为·harmonyos·拖拽事件·ndk ui
不爱吃糖的程序媛5 小时前
开源鸿蒙 Cordova 设备信息插件开发详解
华为·开源·harmonyos
苦逼的搬砖工5 小时前
BLE 通信设计与架构落地
android·flutter
程序员老刘·6 小时前
跨平台开发地图:客户端技术选型指南 | 2025年11月 |(Valdi 加入战场)
flutter·跨平台开发·客户端开发
A懿轩A9 小时前
【2025最新】Flutter 编译开发 鸿蒙HarmonyOS 6 项目教程(Windows)
windows·flutter·华为·openharmony·开源鸿蒙
波儿菜10 小时前
鸿蒙ets实现强制蜂窝网络
harmonyos
江澎涌11 小时前
JHandler——一套简单易用的 C++ 事件循环机制
android·c++·harmonyos