(鸿蒙应用开发)GitCode口袋工具API 封装与搜索功能实现
🎉 恭喜!✅ 创建并修复 GitCode API 客户端(地址、认证、参数)✅ 实现数据模型并解决字段映射 / 类型转换问题✅ 开发搜索页面,支持用户 / 仓库搜索✅ 创建列表页面,实现下拉刷新 / 上拉加载✅ 排查并解决编译、运行、网络等核心问题。
·
本人根据下面文章继续练习实战: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/v4Token 认证失败 认证头格式错误(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 测试步骤(完整流程)
- 生成 Token:
- 访问 GitCode 设置 - 访问令牌
- 勾选权限:
read_user、read_repository - 复制 Token 并保存(仅显示一次)
- 运行应用:
bash
运行
flutter clean flutter pub get flutter run - 测试功能:
- 输入关键字 + 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

更多推荐



所有评论(0)