本人根据下面文章继续练习实战:https://blog.csdn.net/AHuiHatedebug/article/details/155170309?spm=1011.2415.3001.10575&sharefrom=mp_manage_link
本章目标

在本章中,你将学习:

  • 创建 GitCode API 客户端并解决核心配置错误
  • 实现数据模型类并修复字段映射问题
  • 实现用户 / 仓库搜索功能并处理网络异常
  • 创建用户列表和仓库列表页面,实现下拉刷新 / 上拉加载
  • 排查并解决 Flutter 编译、运行、网络请求等常见问题

    第一步:创建 API 客户端基础框架(含问题修复)
     

    1.1 创建 API 客户端文件

    创建 lib/core/gitcode_api.dart注意:原代码存在 API 地址 / 认证方式错误,已修复
     

    import 'package:dio/dio.dart';
    import 'package:flutter/foundation.dart';
    
    /// GitCode API 异常类
    class GitCodeApiException implements Exception {
      const GitCodeApiException(this.message);
      
      final String message;
      
      @override
      String toString() => 'GitCodeApiException: $message';
    }
    
    /// GitCode API 客户端(修复版)
    class GitCodeApiClient {
      GitCodeApiClient({Dio? dio})
          : _dio = dio ??
                Dio(
                  BaseOptions(
                    // 【问题修复】GitCode 官方仅提供 v4 API,v5 地址不存在
                    baseUrl: 'https://gitcode.net/api/v4',
                    connectTimeout: const Duration(seconds: 5),
                    receiveTimeout: const Duration(seconds: 5),
                  ),
                );
    
      final Dio _dio;
    
      /// 构建请求头(【问题修复】GitCode 使用 token 认证,非 Bearer)
      Map<String, String> _buildHeaders(String? personalToken) {
        return {
          if (personalToken != null && personalToken.isNotEmpty)
            'Authorization': 'token $personalToken', // 移除 Bearer 前缀
        };
      }
    }

    1.1.1 常见问题与解决

    问题现象 原因 解决方案
    请求返回 404/401 API 地址错误(v5 不存在) 改为 https://gitcode.net/api/v4
    Token 认证失败 认证头格式错误(Bearer) 改为 token {你的令牌} 格式
    请求超时 网络 / 超时配置过短 保持 5 秒超时,检查网络连接
    第二步:创建数据模型(含字段映射修复)

    2.1 用户搜索模型

    在 lib/core/gitcode_api.dart 文件末尾添加:
     

    /// 搜索用户结果模型(增强空值兜底)
    class GitCodeSearchUser {
      const GitCodeSearchUser({
        required this.login,
        required this.avatarUrl,
        this.name,
        this.htmlUrl,
        this.createdAt,
      });
    
      final String login;        // 登录名
      final String avatarUrl;    // 头像 URL
      final String? name;        // 显示名称
      final String? htmlUrl;     // 主页链接
      final String? createdAt;   // 创建时间
    
      /// 从 JSON 创建对象(【问题修复】增强空值容错)
      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?,
        );
      }
    }
    

    2.2 仓库搜索模型(核心字段修复)

    继续在文件末尾添加:

    /// 仓库模型(【问题修复】适配 GitCode 实际返回字段)
    class GitCodeRepository {
      const GitCodeRepository({
        required this.fullName,
        required this.webUrl,
        this.description,
        this.language,
        this.updatedAt,
        this.stars,
        this.forks,
        this.watchers,
        this.ownerLogin,
        this.isPrivate,
        this.id,
        this.projectId,
      });
    
      final String fullName;     // 完整名称(owner/repo)
      final String webUrl;       // Web URL
      final String? description; // 描述
      final String? language;    // 主要语言
      final String? updatedAt;   // 更新时间
      final int? stars;          // Star 数
      final int? forks;          // Fork 数
      final int? watchers;       // Watch 数
      final String? ownerLogin;  // 所有者
      final bool? isPrivate;     // 是否私有
      final int? id;             // 仓库 ID
      final int? projectId;      // 项目 ID
    
      factory GitCodeRepository.fromJson(Map<String, dynamic> json) {
        return GitCodeRepository(
          // 【问题修复】GitCode 仓库完整名称字段为 path_with_namespace
          fullName: json['path_with_namespace'] as String? ?? json['full_name'] as String? ?? '未知仓库',
          webUrl: json['web_url'] as String? ?? json['html_url'] as String? ?? '',
          description: json['description'] as String?,
          language: json['language'] as String?,
          updatedAt: json['updated_at'] as String?,
          // 【问题修复】Star/Fork 字段名适配 GitCode
          stars: _safeInt(json['star_count'] ?? json['stargazers_count']),
          forks: _safeInt(json['forks_count'] ?? json['forks']),
          watchers: _safeInt(json['watch_count'] ?? json['watchers_count']),
          ownerLogin: (json['owner'] as Map<String, dynamic>?)?['login'] as String?,
          // 【问题修复】私有仓库判断逻辑
          isPrivate: _safeBool(json['visibility'] == 'private' || json['private'] == true),
          id: _safeInt(json['id']),
          projectId: _safeInt(json['project_id']),
        );
      }
    }
    
    /// 安全地将 dynamic 转换为 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;
    }
    
    /// 安全地将 dynamic 转换为 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) {
        return value == '1' || value.toLowerCase() == 'true';
      }
      return null;
    }

2.2.1 常见问题与解决

问题现象 原因 解决方案
repo.name 编译报错 模型中未定义 name 字段 移除 repo.name,仅使用 fullName
仓库名称为空 字段映射错误(full_name 不存在) 改为 path_with_namespace
类型转换异常 JSON 字段类型不匹配(如数字为字符串) 使用 _safeInt/_safeBool 工具

第三步:实现搜索用户 API(含接口路径修复)

3.1 添加搜索用户方法

在 GitCodeApiClient 类中添加:
 

/// 搜索用户(【问题修复】适配 GitCode 接口路径/参数)
Future<List<GitCodeSearchUser>> searchUsers({
  required String keyword,
  required String personalToken,
  int perPage = 10,
  int page = 1,
}) async {
  try {
    debugPrint('搜索用户: $keyword, page: $page');
    
    final response = await _dio.get(
      '/users', // 【问题修复】移除 /search 前缀
      queryParameters: {
        'search': keyword.trim(), // 【问题修复】参数名改为 search(非 q)
        'per_page': perPage.clamp(1, 50),
        'page': page.clamp(1, 100),
      },
      options: Options(
        headers: _buildHeaders(personalToken),
        validateStatus: (status) => status != null && status < 500,
      ),
    );

    final statusCode = response.statusCode ?? 0;
    debugPrint('搜索用户响应状态码: $statusCode');

    if (statusCode == 200) {
      final data = response.data;
      // 【问题修复】GitCode 直接返回数组,非 items 嵌套
      if (data is List<dynamic>) {
        return data
            .whereType<Map<String, dynamic>>()
            .map(GitCodeSearchUser.fromJson)
            .toList();
      }
      return [];
    } else if (statusCode == 401) {
      throw const GitCodeApiException('未授权:Token 无效或已过期');
    } else if (statusCode == 403) {
      throw const GitCodeApiException('禁止访问:Token 缺少 read_user 权限');
    } else {
      throw GitCodeApiException('HTTP 错误: $statusCode');
    }
  } on DioException catch (error) {
    debugPrint('搜索用户 DioException: ${error.type}, ${error.message}');
    
    if (error.type == DioExceptionType.connectionTimeout ||
        error.type == DioExceptionType.receiveTimeout) {
      throw const GitCodeApiException('请求超时,请检查网络连接');
    }
    
    if (error.response?.statusCode == 401) {
      throw const GitCodeApiException('Token 无效或权限不足');
    }
    
    throw GitCodeApiException(error.message ?? '未知网络错误');
  } catch (error) {
    debugPrint('搜索用户异常: $error');
    throw GitCodeApiException('搜索失败: $error');
  }
}

3.1.1 常见问题与解决

问题现象 原因 解决方案
返回空列表 接口路径错误(/search/users) 改为 /users
参数无效 关键字参数名错误(q) 改为 search
数据解析失败 响应格式错误(items 嵌套) GitCode 直接返回数组,无需解析 items
403 错误 Token 权限不足 生成 Token 时勾选 read_user 权限

第四步:实现搜索仓库 API(同用户搜索修复逻辑)

4.1 添加搜索仓库方法

在 GitCodeApiClient 类中继续添加:

Future<List<GitCodeRepository>> searchRepositories({
  required String keyword,
  required String personalToken,
  String? language,
  String? sort,
  String? order,
  int perPage = 10,
  int page = 1,
}) async {
  try {
    debugPrint('搜索仓库: $keyword, page: $page');
    
    final queryParameters = <String, dynamic>{
      'search': keyword.trim(), // 【问题修复】参数名改为 search
      'per_page': perPage.clamp(1, 50),
      'page': page.clamp(1, 100),
    };
    
    // 添加可选参数
    if (language != null && language.isNotEmpty) {
      queryParameters['language'] = language;
    }
    if (sort != null && sort.isNotEmpty) {
      queryParameters['sort'] = sort;
    }
    if (order != null && order.isNotEmpty) {
      queryParameters['order'] = order;
    }

    final response = await _dio.get(
      '/projects', // 【问题修复】移除 /search 前缀
      queryParameters: queryParameters,
      options: Options(
        headers: _buildHeaders(personalToken),
        validateStatus: (status) => status != null && status < 500,
      ),
    );

    final statusCode = response.statusCode ?? 0;
    debugPrint('搜索仓库响应状态码: $statusCode');

    if (statusCode == 200) {
      final data = response.data;
      // 【问题修复】直接解析数组
      if (data is List<dynamic>) {
        return data
            .whereType<Map<String, dynamic>>()
            .map(GitCodeRepository.fromJson)
            .toList();
      }
      return [];
    } else if (statusCode == 401) {
      throw const GitCodeApiException('未授权:Token 无效或已过期');
    } else if (statusCode == 403) {
      throw const GitCodeApiException('禁止访问:Token 缺少 read_repository 权限');
    } else {
      throw GitCodeApiException('HTTP 错误: $statusCode');
    }
  } on DioException catch (error) {
    debugPrint('搜索仓库 DioException: ${error.type}');
    
    if (error.type == DioExceptionType.connectionTimeout ||
        error.type == DioExceptionType.receiveTimeout) {
      throw const GitCodeApiException('请求超时,请检查网络连接');
    }
    
    throw GitCodeApiException(error.message ?? '未知网络错误');
  } catch (error) {
    debugPrint('搜索仓库异常: $error');
    throw GitCodeApiException('搜索失败: $error');
  }
}

第五步:更新搜索页面实现真实搜索(含编译错误修复)

5.1 修改 search_page.dart

打开 lib/pages/main_navigation/search_page.dart,添加导入:
 

import 'package:flutter/material.dart';
import '../../core/gitcode_api.dart';
import '../user_list_page.dart';    // 新增
import '../repository_list_page.dart'; // 新增

在 _SearchPageState 类中添加变量(无修改):

dart

class _SearchPageState extends State<SearchPage> {
  final _client = GitCodeApiClient();  // 添加 API 客户端
  final _keywordController = TextEditingController();
  final _tokenController = TextEditingController();
  
  SearchMode _searchMode = SearchMode.user;
  bool _tokenObscured = true;
  
  // 添加搜索结果相关变量
  bool _isSearching = false;
  String? _errorMessage;
  List<GitCodeSearchUser> _userResults = [];
  List<GitCodeRepository> _repoResults = [];
  // ... 其余代码不变
}

5.1.1 修复 _performSearch 方法(无核心修改)

dart

/// 执行搜索(代码与原文档一致,已适配修复后的 API)
Future<void> _performSearch() async {
  final keyword = _keywordController.text.trim();
  final token = _tokenController.text.trim();
  
  // 输入验证
  if (keyword.isEmpty) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('请输入搜索关键字')),
    );
    return;
  }
  
  if (token.isEmpty) {
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('请输入 Access Token')),
    );
    return;
  }
  
  // 开始搜索
  setState(() {
    _isSearching = true;
    _errorMessage = null;
  });
  
  try {
    if (_searchMode == SearchMode.user) {
      // 搜索用户
      final users = await _client.searchUsers(
        keyword: keyword,
        personalToken: token,
        perPage: 3,  // 预览只显示 3 条
      );
      
      setState(() {
        _userResults = users;
        _isSearching = false;
      });
      
      if (users.isEmpty) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('未找到用户')),
        );
      }
    } else {
      // 搜索仓库
      final repos = await _client.searchRepositories(
        keyword: keyword,
        personalToken: token,
        perPage: 3,
      );
      
      setState(() {
        _repoResults = repos;
        _isSearching = false;
      });
      
      if (repos.isEmpty) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(content: Text('未找到仓库')),
        );
      }
    }
  } on GitCodeApiException catch (e) {
    setState(() {
      _errorMessage = e.message;
      _isSearching = false;
    });
    
    ScaffoldMessenger.of(context).showSnackBar(
      SnackBar(content: Text(e.message)),
    );
  }

5.2 添加搜索结果展示(核心修复:移除 repo.name

5.2.1 修复用户结果展示(头像空值)

dart

/// 用户搜索结果(【问题修复】头像空值容错)
Widget _buildUserResults(ThemeData theme) {
  return Card(
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(
                '搜索结果(${_userResults.length})',
                style: theme.textTheme.titleMedium,
              ),
              TextButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => UserListPage(
                        keyword: _keywordController.text.trim(),
                        token: _tokenController.text.trim(),
                      ),
                    ),
                  );
                },
                child: const Text('查看全部'),
              ),
            ],
          ),
        ),
        ...List.generate(_userResults.length, (index) {
          final user = _userResults[index];
          return ListTile(
            leading: CircleAvatar(
              // 【问题修复】头像 URL 为空时显示默认文字
              backgroundImage: user.avatarUrl.isNotEmpty 
                  ? NetworkImage(user.avatarUrl) 
                  : null,
              child: user.avatarUrl.isEmpty 
                  ? Text(user.login.substring(0, 1).toUpperCase()) 
                  : null,
            ),
            title: Text(user.name ?? user.login),
            subtitle: Text('@${user.login}'),
            trailing: const Icon(Icons.chevron_right),
            onTap: () {
              // TODO: 跳转到用户详情
            },
          );
        }),
      ],
    ),
  );
}
5.2.2 修复仓库结果展示(移除 repo.name

dart

/// 仓库搜索结果(【问题修复】移除不存在的 repo.name 字段)
Widget _buildRepoResults(ThemeData theme) {
  return Card(
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Padding(
          padding: const EdgeInsets.all(16),
          child: Row(
            mainAxisAlignment: MainAxisAlignment.spaceBetween,
            children: [
              Text(
                '搜索结果(${_repoResults.length})',
                style: theme.textTheme.titleMedium,
              ),
              TextButton(
                onPressed: () {
                  Navigator.push(
                    context,
                    MaterialPageRoute(
                      builder: (context) => RepositoryListPage(
                        keyword: _keywordController.text.trim(),
                        token: _tokenController.text.trim(),
                      ),
                    ),
                  );
                },
                child: const Text('查看全部'),
              ),
            ],
          ),
        ),
        ...List.generate(_repoResults.length, (index) {
          final repo = _repoResults[index];
          return ListTile(
            leading: Icon(
              repo.isPrivate == true ? Icons.lock : Icons.folder,
              color: theme.colorScheme.primary,
            ),
            // 【核心修复】移除 repo.name,仅使用 fullName
            title: Text(repo.fullName),
            subtitle: repo.description != null 
                ? Text(
                    repo.description!,
                    maxLines: 1,
                    overflow: TextOverflow.ellipsis,
                  )
                : null,
            trailing: const Icon(Icons.chevron_right),
            onTap: () {
              // TODO: 跳转到仓库详情
            },
          );
        }),
      ],
    ),
  );
}

5.2.3 错误视图(无修改)

dart

/// 错误视图(代码与原文档一致)
Widget _buildErrorView(ThemeData theme) {
  return Card(
    child: Padding(
      padding: const EdgeInsets.all(24),
      child: Column(
        children: [
          Icon(
            Icons.error_outline,
            size: 48,
            color: theme.colorScheme.error,
          ),
          const SizedBox(height: 16),
          Text(
            _errorMessage ?? '搜索失败',
            style: TextStyle(color: theme.colorScheme.error),
            textAlign: TextAlign.center,
          ),
          const SizedBox(height: 16),
          OutlinedButton(
            onPressed: _performSearch,
            child: const Text('重试'),
          ),
        ],
      ),
    ),
  );
}


第六步:创建用户列表页面(含下拉刷新)

6.1 创建用户列表页文件

创建 lib/pages/user_list_page.dart(代码与原文档一致,已适配修复后的 API):

dart

import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../core/gitcode_api.dart';

class UserListPage extends StatefulWidget {
  const UserListPage({
    super.key,
    required this.keyword,
    required this.token,
  });

  final String keyword;
  final String token;

  @override
  State<UserListPage> createState() => _UserListPageState();
}

class _UserListPageState extends State<UserListPage> {
  final _client = GitCodeApiClient();
  final _refreshController = RefreshController();
  
  List<GitCodeSearchUser> _users = [];
  int _currentPage = 1;
  final int _perPage = 20;
  bool _hasMore = true;
  bool _isLoading = false;
  String? _errorMessage;

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

  @override
  void dispose() {
    _refreshController.dispose();
    super.dispose();
  }

  /// 加载用户数据
  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++;
        _isLoading = false;
      });

      if (refresh) {
        _refreshController.refreshCompleted();
      } else {
        _hasMore
            ? _refreshController.loadComplete()
            : _refreshController.loadNoData();
      }
    } on GitCodeApiException catch (e) {
      setState(() {
        _errorMessage = e.message;
        _isLoading = false;
      });

      refresh
          ? _refreshController.refreshFailed()
          : _refreshController.loadFailed();
    }
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('用户搜索: ${widget.keyword}'),
      ),
      body: _buildBody(theme),
    );
  }

  Widget _buildBody(ThemeData theme) {
    // 加载中(首次)
    if (_isLoading && _users.isEmpty && _errorMessage == null) {
      return const Center(child: CircularProgressIndicator());
    }

    // 错误状态
    if (_errorMessage != null && _users.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.error_outline, size: 64, color: Colors.red[300]),
            const SizedBox(height: 16),
            Text(_errorMessage!, style: TextStyle(color: Colors.red[700])),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => _loadUsers(refresh: true),
              child: const Text('重试'),
            ),
          ],
        ),
      );
    }

    // 空状态
    if (_users.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.person_off, size: 64, color: Colors.grey[400]),
            const SizedBox(height: 16),
            Text('未找到用户', style: TextStyle(color: Colors.grey[600])),
          ],
        ),
      );
    }

    // 列表
    return SmartRefresher(
      controller: _refreshController,
      enablePullDown: true,
      enablePullUp: _hasMore,
      header: const ClassicHeader(
        refreshingText: '刷新中...',
        completeText: '刷新完成',
        idleText: '下拉刷新',
        releaseText: '释放刷新',
      ),
      footer: const ClassicFooter(
        loadingText: '加载中...',
        noDataText: '没有更多数据了',
        idleText: '上拉加载更多',
        canLoadingText: '释放加载',
      ),
      onRefresh: () => _loadUsers(refresh: true),
      onLoading: () => _loadUsers(refresh: false),
      child: ListView.builder(
        itemCount: _users.length,
        itemBuilder: (context, index) {
          final user = _users[index];
          return Card(
            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: ListTile(
              leading: CircleAvatar(
                // 【问题修复】头像空值容错
                backgroundImage: user.avatarUrl.isNotEmpty 
                    ? NetworkImage(user.avatarUrl) 
                    : null,
                child: user.avatarUrl.isEmpty 
                    ? Text(user.login.substring(0, 1).toUpperCase()) 
                    : null,
              ),
              title: Text(user.name ?? user.login),
              subtitle: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text('@${user.login}'),
                  if (user.createdAt != null)
                    Text(
                      '加入于 ${user.createdAt!.substring(0, 10)}',
                      style: theme.textTheme.bodySmall,
                    ),
                ],
              ),
              trailing: const Icon(Icons.chevron_right),
              onTap: () {
                // TODO: 跳转到用户详情
                ScaffoldMessenger.of(context).showSnackBar(
                  SnackBar(content: Text('点击了 ${user.login}')),
                );
              },
            ),
          );
        },
      ),
    );
  }
}

第七步:创建仓库列表页面(同用户列表修复逻辑)

7.1 创建仓库列表页文件

创建 lib/pages/repository_list_page.dart(代码与原文档一致,已适配修复后的 API):

dart

import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../core/gitcode_api.dart';

class RepositoryListPage extends StatefulWidget {
  const RepositoryListPage({
    super.key,
    required this.keyword,
    required this.token,
  });

  final String keyword;
  final String token;

  @override
  State<RepositoryListPage> createState() => _RepositoryListPageState();
}

class _RepositoryListPageState extends State<RepositoryListPage> {
  final _client = GitCodeApiClient();
  final _refreshController = RefreshController();
  
  List<GitCodeRepository> _repositories = [];
  int _currentPage = 1;
  final int _perPage = 20;
  bool _hasMore = true;
  bool _isLoading = false;
  String? _errorMessage;

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

  @override
  void dispose() {
    _refreshController.dispose();
    super.dispose();
  }

  Future<void> _loadRepositories({bool refresh = false}) async {
    if (_isLoading) return;

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

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

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

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

      setState(() {
        if (refresh) {
          _repositories = repos;
        } else {
          _repositories.addAll(repos);
        }
        _hasMore = repos.length >= _perPage;
        _currentPage++;
        _isLoading = false;
      });

      if (refresh) {
        _refreshController.refreshCompleted();
      } else {
        _hasMore
            ? _refreshController.loadComplete()
            : _refreshController.loadNoData();
      }
    } on GitCodeApiException catch (e) {
      setState(() {
        _errorMessage = e.message;
        _isLoading = false;
      });

      refresh
          ? _refreshController.refreshFailed()
          : _refreshController.loadFailed();
    }
  }

  @override
  Widget build(BuildContext context) {
    final theme = Theme.of(context);

    return Scaffold(
      appBar: AppBar(
        title: Text('仓库搜索: ${widget.keyword}'),
      ),
      body: _buildBody(theme),
    );
  }

  Widget _buildBody(ThemeData theme) {
    if (_isLoading && _repositories.isEmpty && _errorMessage == null) {
      return const Center(child: CircularProgressIndicator());
    }

    if (_errorMessage != null && _repositories.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.error_outline, size: 64, color: Colors.red[300]),
            const SizedBox(height: 16),
            Text(_errorMessage!, style: TextStyle(color: Colors.red[700])),
            const SizedBox(height: 16),
            ElevatedButton(
              onPressed: () => _loadRepositories(refresh: true),
              child: const Text('重试'),
            ),
          ],
        ),
      );
    }

    if (_repositories.isEmpty) {
      return Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Icon(Icons.folder_off, size: 64, color: Colors.grey[400]),
            const SizedBox(height: 16),
            Text('未找到仓库', style: TextStyle(color: Colors.grey[600])),
          ],
        ),
      );
    }

    return SmartRefresher(
      controller: _refreshController,
      enablePullDown: true,
      enablePullUp: _hasMore,
      header: const ClassicHeader(
        refreshingText: '刷新中...',
        completeText: '刷新完成',
        idleText: '下拉刷新',
        releaseText: '释放刷新',
      ),
      footer: const ClassicFooter(
        loadingText: '加载中...',
        noDataText: '没有更多数据了',
        idleText: '上拉加载更多',
        canLoadingText: '释放加载',
      ),
      onRefresh: () => _loadRepositories(refresh: true),
      onLoading: () => _loadRepositories(refresh: false),
      child: ListView.builder(
        itemCount: _repositories.length,
        itemBuilder: (context, index) {
          final repo = _repositories[index];
          return Card(
            margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
            child: Padding(
              padding: const EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // 标题行
                  Row(
                    children: [
                      Icon(
                        repo.isPrivate == true ? Icons.lock : Icons.folder,
                        color: theme.colorScheme.primary,
                        size: 20,
                      ),
                      const SizedBox(width: 8),
                      Expanded(
                        child: Text(
                          repo.fullName,
                          style: theme.textTheme.titleMedium,
                        ),
                      ),
                    ],
                  ),
                  
                  // 描述
                  if (repo.description != null) ...[
                    const SizedBox(height: 8),
                    Text(
                      repo.description!,
                      style: theme.textTheme.bodyMedium,
                      maxLines: 2,
                      overflow: TextOverflow.ellipsis,
                    ),
                  ],
                  
                  // 统计信息
                  const SizedBox(height: 12),
                  Row(
                    children: [
                      if (repo.language != null) ...[
                        Container(
                          padding: const EdgeInsets.symmetric(
                            horizontal: 8,
                            vertical: 4,
                          ),
                          decoration: BoxDecoration(
                            color: Colors.blue.withOpacity(0.1),
                            borderRadius: BorderRadius.circular(4),
                          ),
                          child: Text(
                            repo.language!,
                            style: const TextStyle(
                              fontSize: 12,
                              color: Colors.blue,
                            ),
                          ),
                        ),
                        const SizedBox(width: 12),
                      ],
                      if (repo.stars != null) ...[
                        const Icon(Icons.star, size: 16, color: Colors.amber),
                        const SizedBox(width: 4),
                        Text('${repo.stars}'),
                        const SizedBox(width: 12),
                      ],
                      if (repo.forks != null) ...[
                        const Icon(Icons.call_split, size: 16),
                        const SizedBox(width: 4),
                        Text('${repo.forks}'),
                      ],
                    ],
                  ),
                ],
              ),
            ),
          );
        },
      ),
    );
  }
}

第八步:Flutter 编译 / 运行常见问题汇总

8.1 编译报错

错误信息 原因 解决方案
The getter 'name' isn't defined for 'GitCodeRepository' 模型中无 name 字段 移除 repo.name,使用 repo.fullName
NetworkImage load failed 头像 URL 为空 添加空值判断,显示默认文字
Building with plugins requires symlink support Windows 符号链接权限不足 开启开发者模式,以管理员运行命令行
unable to find directory entry in pubspec.yaml: assets/images/ 资源目录缺失 在 pubspec.yaml 中添加 assets 配置并创建目录

8.2 运行报错

错误信息 原因 解决方案
Lost connection to device 模拟器连接断开 重启鸿蒙模拟器,指定设备运行:flutter run -d 127.0.0.1:5555
401 Unauthorized Token 无效 / 过期 重新生成 Token,确保权限正确
403 Forbidden Token 权限不足 生成 Token 时勾选 read_user/read_repository
请求超时 网络问题 / API 地址错误 检查网络,确认 API 地址为 https://gitcode.net/api/v4


8.3 测试步骤(完整流程)

  1. 生成 Token
  2. 运行应用

    bash

    运行

    flutter clean
    flutter pub get
    flutter run
    
  3. 测试功能
    • 输入关键字 + Token,测试用户 / 仓库搜索
    • 点击「查看全部」测试下拉刷新 / 上拉加载
    • 验证空数据、错误状态、网络超时等边界情况

本章总结

🎉 恭喜!你已完成:

  • ✅ 创建并修复 GitCode API 客户端(地址、认证、参数)
  • ✅ 实现数据模型并解决字段映射 / 类型转换问题
  • ✅ 开发搜索页面,支持用户 / 仓库搜索
  • ✅ 创建列表页面,实现下拉刷新 / 上拉加载
  • ✅ 排查并解决编译、运行、网络等核心问题

项目结构最终版

plaintext

lib/
├── core/
│   └── gitcode_api.dart          ← API 客户端+数据模型(已修复)
├── pages/
│   ├── main_navigation/
│   │   └── search_page.dart      ← 搜索页面(已修复编译错误)
│   ├── user_list_page.dart       ← 用户列表(下拉刷新)
│   └── repository_list_page.dart ← 仓库列表(下拉刷新)
└── main.dart

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐