【2025版 OpenHarmony】 GitCode 口袋工具:Flutter + Dio 网路请求 打造随身的鸿蒙版 GitCode 搜索助手

GitCode 口袋工具:Flutter + Dio 打造随身的 GitCode 搜索助手

本文完整讲解项目背景、架构设计、关键代码与实战体验,附带上手步骤与扩展建议,适合想快速构建 GitCode API 工具的 Flutter 开发者。


一、背景与目标

  • GitCode 开放搜索 API 需要 Access Token 才能调用,官方界面在小屏设备上操作不便。

  • 我们构建一个轻量「口袋工具」:输入关键字 + Token 即可查询用户或仓库。

  • 技术栈:Flutter 3.x、Material 3 设计体系、Dio 网络库;整体结构遵循「配置层 + 网络层 + UI 层」。

项目展示:

可以查询用户:

可以查询仓库:


二、工程结构概览

复制代码
lib/
├─ core/
│  ├─ app_config.dart         // 工程级配置(例如 demo token)
│  └─ gitcode_api.dart        // GitCode API 封装(Dio)
└─ main.dart                  // Flutter UI 与交互逻辑

依赖配置:

pubspec.yaml

Dart 复制代码
  # 提供 iOS 风格(Cupertino 风格)的图标。
  cupertino_icons: ^1.0.8 

  # 一个强大的 HTTP 网络请求库,用于发送网络请求(GET、POST、PUT、DELETE 等)。
  dio: ^5.7.0

2.1 配置层:AppConfig

Dart 复制代码
1:5:lib/core/app_config.dart
/// 应用级别的常量配置统一放在这里,便于集中管理与调优。
class AppConfig {
  AppConfig._();
​
  /// 体验用的占位 Token。正式环境请替换为你自己的 GitCode 访问令牌,
  /// 并优先考虑使用安全存储或远程配置方案,避免泄露敏感信息。
  static const demoToken = '<你的 GitCode 个人访问令牌>';
}
  • 实际部署时将 demoToken 替换为自己的 Personal Access Token。

  • 建议通过 .env 或远程配置管理生产环境 Token,这里仅用于演示。

2.2 网络层:GitCodeApiClient

  • 基于 Dio 封装了用户查询、搜索用户、搜索仓库等接口。

  • 统一超时 5 秒,所有请求都带 Authorization: Bearer <token> 头部。

  • 使用 GitCodeApiException 统一承载错误信息,方便 UI 层展示友好提示。

2.3 UI 层:GitCodePocketToolApp

  • Material 3 主题 + SegmentedButton 控制搜索模式。

  • Form + TextFormField 做输入校验;FilledButton.icon 触发查询,含加载态。

  • 结果区复用 _UserTile / _RepoTile 两种组件,保证结构清晰。


三、GitCode API Client 总览

  • 文件 lib/core/gitcode_api.dart

  • 功能:统一封装 GitCode REST v5 API 的请求细节(超时、鉴权、错误处理)并提供用户/仓库搜索模型。

  • 使用场景:Flutter/OpenHarmony 应用中查询 GitCode 用户资料、搜索用户与仓库。


3.1 架构速览

  • GitCodeApiClient :持有 Dio 实例,暴露 fetchUsersearchUserssearchRepositories 三个公开方法以及一个内部搜索降级 _searchLoginByKeyword、统一请求头 _buildHeaders

  • 模型层GitCodeUserGitCodeSearchUserGitCodeRepository;以及两个工具函数 _safeInt_safeBool 保证解析鲁棒性。

  • 错误建模GitCodeApiException 对外抛出用户友好文案,避免泄露底层异常。


3.2 请求流程详解

  1. 构造客户端

    • 可注入自定义 Dio(便于测试/Mock)。

    • 默认 Base URL https://api.gitcode.com/api/v5,5 秒连接和接收超时。

  2. fetchUser(username, personalToken)

    • 先 trim、校验非空,再发起 /users/{login} 请求。

    • 响应码分支:

      • 200:直接解析 GitCodeUser

      • 401:提示 token 未授权。

      • 404:若允许且携带 token,则调用 _searchLoginByKeyword,获取真实 login 后二次请求;否则提示未找到。

      • 其他:抛出通用失败。

    • 捕获 DioException,针对 connectionTimeout/receiveTimeoutbadResponse 给出定制信息。

  3. searchUsers(keyword, personalToken, perPage, page)

    • 强制携带 token,支持分页。

    • 校验 keyword 非空,perPage [1,50],page [1,100]。

    • 401 → token 权限不足;200 且 body 存在 → 映射为 GitCodeSearchUser

  4. searchRepositories(keyword, personalToken, {language, sort, order})

    • 参数校验逻辑与用户搜索类似,额外对语言、排序字段做 optional 透传。

    • 优先读取 stargazers_count,若无则回退 stars_count

  5. 降级搜索 _searchLoginByKeyword

    • 必须携带 token,命中 401 直接提示。

    • 只拉取 per_page=1 的最相关结果,提取 login 字段供 fetchUser 二次调用。

    • 任何 DioException 都吞掉并返回 null,避免打断主流程。

  6. 通用请求头 _buildHeaders

    • 仅在 token 非空场景附加 Authorization: Bearer <token>,避免无意义 header。

3.3 数据模型与解析策略

  • GitCodeUser :展示在个人主页的完整信息;_safeInt 确保 JSON int/string 兼容;createdAt 保留原始 ISO 字符串,交由 UI 决定格式。

  • GitCodeSearchUser:搜索列表项,字段较少,保留 avatar/name/homepage。

  • GitCodeRepository

    • owner 结构兼容 name/path。

    • isPrivate 通过 _safeBool 兼容 0/1、'true'/'false'。

    • stars 兼容历史字段名称。

  • 异常类 GitCodeApiException:所有对外错误均使用人类可读 message。


3.4 使用建议

  • 幂等性 :同一 login 重复调用 fetchUser 没有副作用,可配合缓存。

  • Token 策略:公共数据 API 可匿名访问,但 404 时的 search fallback 依赖 token;建议在设置页提示用户填入。

  • 错误提示 :直接使用 GitCodeApiException.message 显示给用户即可;其它异常使用通用重试提示。

  • 测试 :可注入 Mock Dio,模拟不同 status code 和异常类型。


3.5 代码详解

以下代码保持与 lib/core/gitcode_api.dart 一致,并按块拆分,每段代码前都附带用途说明,便于逐段理解。

1. 客户端构造与依赖

导入 dio,初始化 GitCodeApiClient 及默认超时配置。

Dart 复制代码
import 'package:dio/dio.dart';
​
/// GitCode API 的轻量封装,负责统一请求参数、超时和错误处理。
class GitCodeApiClient {
  GitCodeApiClient({Dio? dio})
      : _dio = dio ??
            Dio(
              BaseOptions(
                baseUrl: 'https://api.gitcode.com/api/v5', // GitCode REST v5
                connectTimeout: const Duration(seconds: 5), // 快速失败,保证 UI 体验
                receiveTimeout: const Duration(seconds: 5),
              ),
            );
​
  final Dio _dio;

2. fetchUser:获取用户详情与搜索降级

校验用户名→请求 /users/{login}→按状态码处理→必要时 fallback 走搜索。

复制代码
  
Dart 复制代码
/// 通过登录名获取用户详情,可选地携带 token 进行授权访问。
  Future<GitCodeUser> fetchUser(
    String username, {
    String? personalToken,
    bool allowSearchFallback = true, // 首次失败时可尝试模糊搜索获取真实 login
  }) async {
    final trimmed = username.trim();
    if (trimmed.isEmpty) {
      throw const GitCodeApiException('用户名不能为空');
    }
​
    try {
      final response = await _dio.get<Map<String, dynamic>>(
        '/users/${Uri.encodeComponent(trimmed)}',
        options: Options(
          headers: _buildHeaders(personalToken),
          responseType: ResponseType.json,
          validateStatus: (status) => status != null && status < 500, // 统一在代码里处理 4xx
        ),
      );
​
      final statusCode = response.statusCode ?? 0;
      switch (statusCode) {
        case 200:
          final data = response.data;
          if (data == null) {
            throw const GitCodeApiException('接口返回为空,请稍后重试');
          }
          return GitCodeUser.fromJson(data);
        case 401:
          throw const GitCodeApiException('未授权,请检查 access token 或权限');
        case 404:
          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, // 防止递归 fallback
              );
            }
          }
          throw GitCodeApiException(
            '未找到用户 $trimmed,若输入的是展示昵称,请改用登录名或携带 token 搜索。',
          );
        default:
          throw GitCodeApiException(
            '查询失败 (HTTP $statusCode),请稍后重试',
          );
      }
    } on DioException catch (error) {
      if (error.type == DioExceptionType.connectionTimeout ||
          error.type == DioExceptionType.receiveTimeout) {
        throw const GitCodeApiException('请求超时,请检查网络后重试');
      }
      if (error.type == DioExceptionType.badResponse) {
        throw GitCodeApiException(
          '请求异常:HTTP ${error.response?.statusCode ?? '-'}',
        );
      }
      throw GitCodeApiException(error.message ?? '未知网络错误');
    }
  }

3. _buildHeaders:按需附加鉴权头

当 token 存在时才拼接 Bearer,避免空 header。

Dart 复制代码
/// 构造通用请求头,必要时附加 Bearer token。
  Map<String, String> _buildHeaders(String? personalToken) {
    return {
      if (personalToken != null && personalToken.isNotEmpty)
        'Authorization': 'Bearer $personalToken',
    };
  }

4. _searchLoginByKeyword:昵称到 login 的降级映射

命中 404 时调用搜索接口尝试获取真实 login,异常时静默返回 null

Dart 复制代码
/// 当用户输入昵称时,通过搜索接口尝试获取真实 login。
  Future<String?> _searchLoginByKeyword(
    String keyword, {
    required String personalToken,
  }) async {
    try {
      final response = await _dio.get<List<dynamic>>(
        '/search/users',
        queryParameters: {
          'q': keyword,
          'per_page': 1, // 只取第一个最相关结果即可
          'access_token': personalToken,
        },
        options: Options(
          headers: _buildHeaders(personalToken),
          responseType: ResponseType.json,
          validateStatus: (status) => status != null && status < 500,
        ),
      );
​
      if ((response.statusCode ?? 0) == 200) {
        final list = response.data;
        if (list == null || list.isEmpty) {
          return null;
        }
        final first = list.first;
        if (first is Map<String, dynamic>) {
          return first['login'] as String?;
        }
      } else if ((response.statusCode ?? 0) == 401) {
        throw const GitCodeApiException('搜索需要有效的 access token');
      }
    } on DioException {
      return null; // 搜索失败不影响主流程,静默降级
    }
    return null;
  }

5. searchUsers:带分页的用户搜索

强制要求 token,校验分页参数范围并映射结果集合。

Dart 复制代码
/// 调用 `/search/users`,返回符合关键字的用户简要信息。
  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,
        '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();
  }

6. searchRepositories:仓库搜索及可选过滤

与用户搜索类似,额外支持语言、排序、顺序等可选参数。

Dart 复制代码
/// 调用 `/search/repositories`,支持语言、排序等可选参数。
  Future<List<GitCodeRepository>> searchRepositories({
    required String keyword,
    required String personalToken,
    String? language,
    String? sort,
    String? order,
    int perPage = 10,
    int page = 1,
  }) async {
    final trimmed = keyword.trim();
    if (trimmed.isEmpty) {
      throw const GitCodeApiException('请输入搜索关键字');
    }
​
    final queryParameters = <String, dynamic>{
      'q': trimmed,
      'access_token': personalToken,
      'per_page': perPage.clamp(1, 50),
      'page': page.clamp(1, 100),
      if (language != null && language.isNotEmpty) 'language': language,
      if (sort != null && sort.isNotEmpty) 'sort': sort,
      if (order != null && order.isNotEmpty) 'order': order,
    };
​
    final response = await _dio.get<List<dynamic>>(
      '/search/repositories',
      queryParameters: queryParameters,
      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(GitCodeRepository.fromJson)
        .toList();
  }
}

7. GitCodeUser 模型

描述完整用户信息,兼容缺失字段。

Dart 复制代码
class GitCodeUser {
  GitCodeUser({
    required this.login,
    required this.avatarUrl,
    this.name,
    this.bio,
    this.htmlUrl,
    this.publicRepos,
    this.followers,
    this.following,
    this.createdAt,
  });

  factory GitCodeUser.fromJson(Map<String, dynamic> json) {
    return GitCodeUser(
      login: json['login'] as String? ?? '',
      avatarUrl: json['avatar_url'] as String? ?? '',
      name: json['name'] as String?,
      bio: json['bio'] as String?,
      htmlUrl: json['html_url'] as String?,
      publicRepos: _safeInt(json['public_repos']),
      followers: _safeInt(json['followers']),
      following: _safeInt(json['following']),
      createdAt: json['created_at'] as String?,
    );
  }

  /// GitCode 唯一登录名,可用于后续接口请求。
  final String login;

  /// 头像的完整 URL,用于展示用户头像。
  final String avatarUrl;

  /// 用户在个人资料中填写的展示昵称,可能为空。
  final String? name;

  /// 个人简介或签名信息。
  final String? bio;

  /// 用户主页链接,通常指向 GitCode Web。
  final String? htmlUrl;

  /// 用户公开仓库的数量。
  final int? publicRepos;

  /// 粉丝(followers)数量。
  final int? followers;

  /// 关注(following)数量。
  final int? following;

  /// 账号创建时间,ISO 时间字符串。
  final String? createdAt;
}

8. _safeInt:宽松的整型转换

intString 类型进行兼容解析。

Dart 复制代码
/// 将接口返回的动态数值转换为 int,兼容字符串与数字。
int? _safeInt(dynamic value) {
  if (value == null) {
    return null;
  }
  if (value is int) {
    return value;
  }
  if (value is String) {
    return int.tryParse(value);
  }
  return null;
}
9. GitCodeApiException:统一异常出口
封装用户可读的错误信息。

class GitCodeApiException implements Exception {
  const GitCodeApiException(this.message);

  final String message;

  @override
  String toString() => 'GitCodeApiException: $message';
}

10. GitCodeSearchUser:搜索列表模型

用于展示搜索结果中的用户条目。

Dart 复制代码
class GitCodeSearchUser {
  GitCodeSearchUser({
    required this.login,
    required this.avatarUrl,
    this.name,
    this.htmlUrl,
    this.createdAt,
  });

  factory GitCodeSearchUser.fromJson(Map<String, dynamic> json) {
    return GitCodeSearchUser(
      login: json['login'] as String? ?? '',
      avatarUrl: json['avatar_url'] as String? ?? '',
      name: json['name'] as String?,
      htmlUrl: json['html_url'] as String?,
      createdAt: json['created_at'] as String?,
    );
  }

  /// 搜索结果中的登录名。
  final String login;

  /// 搜索结果中的头像地址。
  final String avatarUrl;

  /// 搜索结果中的展示名称。
  final String? name;

  /// 用户主页链接。
  final String? htmlUrl;

  /// 用户创建时间。
  final String? createdAt;
}

11. GitCodeRepository:仓库搜索模型

兼容多种字段命名与 owner 结构。

Dart 复制代码
class GitCodeRepository {
  GitCodeRepository({
    required this.fullName,
    required this.webUrl,
    this.description,
    this.language,
    this.updatedAt,
    this.stars,
    this.forks,
    this.watchers,
    this.ownerLogin,
    this.isPrivate,
  });

  factory GitCodeRepository.fromJson(Map<String, dynamic> json) {
    final owner = json['owner'];
    return GitCodeRepository(
      fullName: json['full_name'] as String? ?? '',
      webUrl: json['web_url'] as String? ?? '',
      description: json['description'] as String?,
      language: json['language'] as String?,
      updatedAt: json['updated_at'] as String?,
      stars: _safeInt(json['stargazers_count']) ?? _safeInt(json['stars_count']),
      forks: _safeInt(json['forks_count']),
      watchers: _safeInt(json['watchers_count']),
      ownerLogin: owner is Map<String, dynamic>
          ? owner['name'] as String? ?? owner['path'] as String?
          : null,
      isPrivate: _safeBool(json['private']),
    );
  }

  /// 仓库的全名,形如 "owner/repo"。
  final String fullName;

  /// 仓库在 GitCode 上的访问 URL。
  final String webUrl;

  /// 仓库描述,可能为空。
  final String? description;

  /// 主要编程语言。
  final String? language;

  /// 最近一次更新时间。
  final String? updatedAt;

  /// Stargazers 数量,优先读取 `stargazers_count`。
  final int? stars;

  /// Fork 数量。
  final int? forks;

  /// Watchers 数量。
  final int? watchers;

  /// 仓库所有者的登录名或路径。
  final String? ownerLogin;

  /// 是否私有仓库。
  final bool? isPrivate;
}

12. _safeBool:多格式布尔值解析

兼容 0/1、true/false 以及大小写差异。

Dart 复制代码
/// GitCode 接口可能返回 0/1、'true'/'false',统一转成 bool。
bool? _safeBool(dynamic value) {
  if (value == null) {
    return null;
  }
  if (value is bool) {
    return value;
  }
  if (value is int) {
    return value != 0;
  }
  if (value is String) {
    if (value == '1' || value.toLowerCase() == 'true') {
      return true;
    }
    if (value == '0' || value.toLowerCase() == 'false') {
      return false;
    }
  }
  return null;
}

3.6 官方借鉴网站

搜索用户文档:

搜索用户 | GitCode 帮助文档https://docs.gitcode.com/docs/apis/get-api-v-5-search-users搜索仓库文档:

搜索仓库 | GitCode 帮助文档https://docs.gitcode.com/docs/apis/get-api-v-5-search-repositories

四、关键代码与设计解析

4.1 Material App 与首页结构

Dart 复制代码
12:135:lib/main.dart
class GitCodePocketToolApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'GitCode 口袋工具',
      theme: ThemeData(
        colorSchemeSeed: Colors.indigo,
        useMaterial3: true,
      ),
      home: const GitCodePocketToolPage(),
    );
  }
}
  • colorSchemeSeed 仅需一个主色即可生成整套 Material 3 调色板。

  • GitCodePocketToolPage 是核心状态组件,负责输入、网络请求与结果展示。

4.2 状态与搜索流程

Dart 复制代码
36:214:lib/main.dart
class _GitCodePocketToolPageState extends State<GitCodePocketToolPage> {
  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;

  Future<void> _performSearch() async {
    if (_isLoading) return;
    if (!_formKey.currentState!.validate()) return;

    FocusScope.of(context).unfocus();
    setState(() { _isLoading = true; _errorMessage = null; ... });

    final token = _tokenController.text.trim();
    try {
      if (_mode == QueryMode.user) {
        final users = await _client.searchUsers(...);
        setState(() => _userResults = users);
      } else {
        final repos = await _client.searchRepositories(...);
        setState(() => _repoResults = repos);
      }
    } on GitCodeApiException catch (error) {
      setState(() => _errorMessage = error.message);
    } catch (error) {
      setState(() => _errorMessage = '未知错误:$error');
    } finally {
      if (mounted) {
        setState(() => _isLoading = false);
      }
    }
  }
}
  • 通过 _mode 控制两种搜索逻辑;结果列表在 setState 中更新。

  • 所有异常被转换成 _errorMessage 再交给 _InfoBanner 展示。

4.3 搜索模式切换

Dart 复制代码
239:266:lib/main.dart
Widget _buildModeSwitcher() {
  return SegmentedButton<QueryMode>(
    segments: const [
      ButtonSegment(value: QueryMode.user, icon: Icon(Icons.person_outline), label: Text('用户')),
      ButtonSegment(value: QueryMode.repository, icon: Icon(Icons.bookmark_outline), label: Text('仓库')),
    ],
    selected: <QueryMode>{_mode},
    onSelectionChanged: (selection) {
      final next = selection.first;
      if (next != _mode) {
        setState(() {
          _mode = next;
          _errorMessage = null;
          _userResults = const [];
          _repoResults = const [];
        });
      }
    },
  );
}
  • 切换模式时顺便清空历史结果和错误,避免状态污染。

4.4 结果卡片组件

Dart 复制代码
382:430:lib/main.dart
class _RepoTile extends StatelessWidget {
  const _RepoTile(this.repo);

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 0,
      margin: const EdgeInsets.only(bottom: 12),
      child: ListTile(
        leading: const Icon(Icons.folder_outlined),
        title: Text(repo.fullName, style: const TextStyle(fontWeight: FontWeight.w600)),
        subtitle: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            if (repo.description?.isNotEmpty == true)
              Padding(
                padding: const EdgeInsets.only(bottom: 4),
                child: Text(repo.description!, maxLines: 2, overflow: TextOverflow.ellipsis),
              ),
            Wrap(
              spacing: 8,
              runSpacing: 4,
              children: [
                if (repo.language != null) _StatChip(label: '语言', valueLabel: repo.language),
                _StatChip(label: 'Star', value: repo.stars),
                _StatChip(label: 'Fork', value: repo.forks),
              ],
            ),
            const SizedBox(height: 4),
            Text('更新 ${repo.updatedAt ?? '-'}', style: const TextStyle(fontSize: 12)),
            Text(repo.webUrl, style: const TextStyle(fontSize: 12, color: Colors.blue),
          ],
        ),
      ),
    );
  }
}
  • Wrap 让统计标签在小屏仍能自动换行。

  • _StatChip 复用 Chip 样式,显示数值 + 标签,保持一致视觉。

4.5 API 客户端亮点

Dart 复制代码
3:214:lib/core/gitcode_api.dart
class GitCodeApiClient {
  GitCodeApiClient({Dio? dio})
      : _dio = dio ?? Dio(BaseOptions(
          baseUrl: 'https://api.gitcode.com/api/v5',
          connectTimeout: const Duration(seconds: 5),
          receiveTimeout: const Duration(seconds: 5),
        ));
​
  Future<List<GitCodeSearchUser>> searchUsers({ ... }) async {
    final response = await _dio.get<List<dynamic>>(
      '/search/users',
      queryParameters: {
        'q': trimmed,
        'access_token': personalToken,
        'per_page': perPage.clamp(1, 50),
        'page': page.clamp(1, 100),
      },
      options: Options(
        headers: _buildHeaders(personalToken),
        validateStatus: (status) => status != null && status < 500,
      ),
    );
    if ((response.statusCode ?? 0) == 401) {
      throw const GitCodeApiException('Token 无效或权限不足,无法搜索用户');
    }
    if (response.statusCode != 200 || response.data == null) {
      throw GitCodeApiException('搜索用户失败 (HTTP $statusCode)');
    }
    return response.data!
        .whereType<Map<String, dynamic>>()
        .map(GitCodeSearchUser.fromJson)
        .toList();
  }
}
  • per_pagepage 做范围限制,避免过大分页影响体验。

  • validateStatus 允许 4xx 在 response 中返回,自行处理错误文案。

  • _searchLoginByKeyword 作为兜底逻辑,在用户输入昵称时仍尽量返回正确数据。


五、环境准备与运行步骤

5.1 环境准备

【2025最新】Flutter 编译开发 鸿蒙HarmonyOS 6 项目教程(Windows)_flutter build app 鸿蒙-CSDN博客https://blog.csdn.net/2301_80035882/article/details/155001657?spm=1011.2415.3001.5331根据上述已经配置好环境。

5.2 下载仓库压缩包

仓库地址:

gitcode_pocket_tool - AtomGit | GitCodehttps://gitcode.com/byyixuan/gitcode_pocket_tool

5.3 配置访问令牌

GitCode - 全球开发者的开源社区,开源代码托管平台https://gitcode.com/setting/token-classic

配置令牌名称和到期时间,之后向下滑找到新建访问令牌按钮,点击此按钮新建:

注意重要:新建成功后,一定要复制好这个访问令牌,并且保存好,不要让别人知道!

5.4 解压项目并打开

打开之后目录如图所示:

点击app_config.dart文件,并将你的访问令牌复制到此位置:

注意:这只是调式项目仅供学习参考,正式环境下,需要将访问令牌隐式配置到本地,不在其中显示。

5.5 开始启动项目

替换成功后,在DevEco Studio中编译运行此项目,点击右上角绿色运行按钮,启动过程中耐心等待。注意,虚拟机启动需要保证虚拟机连接上网络:

如图所示启动成功:

5.6 项目展示

可以查询用户:

可以查询仓库:

相关推荐
f***24111 小时前
不常用,总是忘记:nginx 重启指令
运维·windows·nginx
de之梦-御风1 小时前
【远程控制】RustDesk 自建服务端完整方案(Docker + Windows 客户端)
windows·docker·容器
SuperHeroWu71 小时前
鸿蒙应用如何实现内存级别全局缓存数据?
华为·harmonyos·单例·lrucache·appstroage·内存缓存
气概1 小时前
WINDOWS系统安装
windows
QuantumLeap丶1 小时前
《Flutter全栈开发实战指南:从零到高级》- 20 -主题与国际化
flutter·ios·前端框架
心随雨下1 小时前
Flutter动画系统详解
flutter
g***86691 小时前
Windows上安装Go并配置环境变量(图文步骤)
开发语言·windows·golang
小龙报1 小时前
VS2022调试技巧 + 实战案例
android·服务器·c语言·数据库·c++·windows·visual studio
2301_795167201 小时前
Python 高手编程系列一十三:现实例子 — 延迟求值属性
开发语言·windows·python