【开源鸿蒙跨平台开发】AtomGit 口袋工具 API 封装与搜索功能实现
> 欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
此功能的实现基于上一篇文章:
【Flutter for OpenHarmony】AtomGit 口袋工具应用开发实践(开源鸿蒙跨平台项目实操)-CSDN博客
这篇文章是跟练实践,补充本人遇到的一些问题以及解决方法(文章最后是补充部分)
注:实际项目路径已调整为 atomgit-pocket-tool(原 gitcode-pocket-tool 对应更名)
第一步:创建 API 客户端基础框架
创建 lib/core/atomcode_api.dart

内容如下:
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';/// AtomCode API 异常类
class AtomCodeApiException implements Exception {
const AtomCodeApiException(this.message);
final String message;
@override
String toString() => 'AtomCodeApiException: $message';
}/// AtomCode API 客户端
class AtomCodeApiClient {
AtomCodeApiClient({Dio? dio})
: _dio = dio ??
Dio(
BaseOptions(
baseUrl: 'https://api.atomcode.com/api/v5',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
),
);final Dio _dio;
/// 构建请求头
Map<String, String> _buildHeaders(String? personalToken) {
return {
if (personalToken != null && personalToken.isNotEmpty)
'Authorization': 'Bearer $personalToken',
};
}
}
代码说明:
AtomCodeApiException:自定义异常类,用于包装 API 错误
AtomCodeApiClient:API 客户端,使用 Dio 发送 HTTP 请求
baseUrl:GitCode API v5 基础地址
connectTimeout 和 receiveTimeout:5 秒超时
_buildHeaders:构建请求头,支持 Bearer Token 认证
第二步:创建数据模型
2.1 用户搜索模型
在 lib/core/atomcode_api.dart 文件末尾添加:
/// 搜索用户结果模型
class AtomCodeSearchUser {
const AtomCodeSearchUser({
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 AtomCodeSearchUser.fromJson(Map<String, dynamic> json) {
return AtomCodeSearchUser(
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?,
);
}
}
代码说明:
login:用户登录名(必需)avatarUrl:头像地址(必需)name、htmlUrl、createdAt:可选字段fromJson:工厂构造函数,从 JSON 创建对象
2.2 仓库搜索模型
继续在文件末尾添加:
/// 仓库模型
class AtomCodeRepository {
const AtomCodeRepository({
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; // 项目 IDfactory AtomCodeRepository.fromJson(Map<String, dynamic> json) {
return AtomCodeRepository(
fullName: json['full_name'] as String? ?? json['path_with_namespace'] 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?,
stars: _safeInt(json['stargazers_count'] ?? json['star_count']),
forks: _safeInt(json['forks_count'] ?? json['forks']),
watchers: _safeInt(json['watchers_count'] ?? json['watchers']),
ownerLogin: (json['owner'] as Map<String, dynamic>?)?['login'] as String?,
isPrivate: _safeBool(json['private'] ?? json['visibility'] == 'private'),
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;
}
第三步:实现搜索用户 API
3.1 添加搜索用户方法
在 AtomCodeApiClient 类中添加:
/// 搜索用户
Future<List<AtomCodeSearchUser>> searchUsers({
required String keyword,
required String personalToken,
int perPage = 10,
int page = 1,
}) async {
try {
debugPrint('搜索用户: $keyword, page: $page');
final response = await _dio.get(
'/search/users',
queryParameters: {
'access_token': personalToken,
'q': keyword.trim(),
'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;
if (data is Map<String, dynamic>) {
final items = data['items'] as List<dynamic>?;
if (items != null) {
return items
.whereType<Map<String, dynamic>>()
.map(AtomCodeSearchUser.fromJson)
.toList();
}
}
return [];
} else if (statusCode == 401) {
throw const AtomCodeApiException('未授权,请检查 Token 是否正确');
} else if (statusCode == 404) {
throw const AtomCodeApiException('未找到用户');
} else {
throw AtomCodeApiException('HTTP 错误: $statusCode');
}
} on DioException catch (error) {
debugPrint('搜索用户 DioException: ${error.type}, ${error.message}');
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout) {
throw const AtomCodeApiException('请求超时,请检查网络连接');
}
if (error.response?.statusCode == 401) {
throw const AtomCodeApiException('Token 无效或权限不足');
}
throw AtomCodeApiException(error.message ?? '未知网络错误');
} catch (error) {
debugPrint('搜索用户异常: $error');
throw AtomCodeApiException('搜索失败: $error');
}
}
代码说明:
使用 _dio.get 发送 GET 请求
路径:/search/users
参数:access_token、q(关键字)、per_page、page
使用 clamp 限制参数范围
返回 List<AtomCodeSearchUser>
完善的错误处理:401(未授权)、404(未找到)、超时等
以下是完整的 AtomCodeApiClient 类结构(包含第三步添加的 searchUsers 方法):
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
/// AtomCode API 异常类
class AtomCodeApiException implements Exception {
const AtomCodeApiException(this.message);
final String message;
@override
String toString() => 'AtomCodeApiException: $message';
}
/// AtomCode API 客户端
class AtomCodeApiClient {
AtomCodeApiClient({Dio? dio})
: _dio = dio ??
Dio(
BaseOptions(
baseUrl: 'https://api.atomcode.com/api/v5',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
),
);
final Dio _dio;
/// 构建请求头
Map<String, String> _buildHeaders(String? personalToken) {
return {
if (personalToken != null && personalToken.isNotEmpty)
'Authorization': 'Bearer $personalToken',
};
}
// ====================== 第三步添加的位置 ======================
/// 搜索用户
Future<List<AtomCodeSearchUser>> searchUsers({
required String keyword,
required String personalToken,
int perPage = 10,
int page = 1,
}) async {
try {
debugPrint('搜索用户: $keyword, page: $page');
final response = await _dio.get(
'/search/users',
queryParameters: {
'access_token': personalToken,
'q': keyword.trim(),
'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;
if (data is Map<String, dynamic>) {
final items = data['items'] as List<dynamic>?;
if (items != null) {
return items
.whereType<Map<String, dynamic>>()
.map(AtomCodeSearchUser.fromJson)
.toList();
}
}
return [];
} else if (statusCode == 401) {
throw const AtomCodeApiException('未授权,请检查 Token 是否正确');
} else if (statusCode == 404) {
throw const AtomCodeApiException('未找到用户');
} else {
throw AtomCodeApiException('HTTP 错误: $statusCode');
}
} on DioException catch (error) {
debugPrint('搜索用户 DioException: ${error.type}, ${error.message}');
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout) {
throw const AtomCodeApiException('请求超时,请检查网络连接');
}
if (error.response?.statusCode == 401) {
throw const AtomCodeApiException('Token 无效或权限不足');
}
throw AtomCodeApiException(error.message ?? '未知网络错误');
} catch (error) {
debugPrint('搜索用户异常: $error');
throw AtomCodeApiException('搜索失败: $error');
}
}
// ====================== 第三步添加结束 ======================
}
// 以下是第二步添加的模型类(位置不变)
/// 搜索用户结果模型
class AtomCodeSearchUser {
const AtomCodeSearchUser({
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 AtomCodeSearchUser.fromJson(Map<String, dynamic> json) {
return AtomCodeSearchUser(
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?,
);
}
}
/// 仓库模型
class AtomCodeRepository {
const AtomCodeRepository({
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 AtomCodeRepository.fromJson(Map<String, dynamic> json) {
return AtomCodeRepository(
fullName: json['full_name'] as String? ?? json['path_with_namespace'] 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?,
stars: _safeInt(json['stargazers_count'] ?? json['star_count']),
forks: _safeInt(json['forks_count'] ?? json['forks']),
watchers: _safeInt(json['watchers_count'] ?? json['watchers']),
ownerLogin: (json['owner'] as Map<String, dynamic>?)?['login'] as String?,
isPrivate: _safeBool(json['private'] ?? json['visibility'] == 'private'),
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;
}
第四步:实现搜索仓库 API
4.1 添加搜索仓库方法
在 AtomCodeApiClient 类中继续添加:
/// 搜索仓库
Future<List<AtomCodeRepository>> 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>{
'access_token': personalToken,
'q': keyword.trim(),
'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(
'/search/repositories',
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 Map<String, dynamic>) {
final items = data['items'] as List<dynamic>?;
if (items != null) {
return items
.whereType<Map<String, dynamic>>()
.map(AtomCodeRepository.fromJson)
.toList();
}
}
return [];
} else if (statusCode == 401) {
throw const AtomCodeApiException('未授权,请检查 Token 是否正确');
} else if (statusCode == 404) {
throw const AtomCodeApiException('未找到仓库');
} else {
throw AtomCodeApiException('HTTP 错误: $statusCode');
}
} on DioException catch (error) {
debugPrint('搜索仓库 DioException: ${error.type}');
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout) {
throw const AtomCodeApiException('请求超时,请检查网络连接');
}
throw AtomCodeApiException(error.message ?? '未知网络错误');
} catch (error) {
debugPrint('搜索仓库异常: $error');
throw AtomCodeApiException('搜索失败: $error');
}
}
以下是更新后的完整 AtomCodeApiClient 类结构(包含第四步的 searchRepositories 方法):
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
/// AtomCode API 异常类
class AtomCodeApiException implements Exception {
const AtomCodeApiException(this.message);
final String message;
@override
String toString() => 'AtomCodeApiException: $message';
}
/// AtomCode API 客户端
class AtomCodeApiClient {
AtomCodeApiClient({Dio? dio})
: _dio = dio ??
Dio(
BaseOptions(
baseUrl: 'https://api.atomcode.com/api/v5',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
),
);
final Dio _dio;
/// 构建请求头
Map<String, String> _buildHeaders(String? personalToken) {
return {
if (personalToken != null && personalToken.isNotEmpty)
'Authorization': 'Bearer $personalToken',
};
}
/// 搜索用户(第三步添加的方法)
Future<List<AtomCodeSearchUser>> searchUsers({
required String keyword,
required String personalToken,
int perPage = 10,
int page = 1,
}) async {
try {
debugPrint('搜索用户: $keyword, page: $page');
final response = await _dio.get(
'/search/users',
queryParameters: {
'access_token': personalToken,
'q': keyword.trim(),
'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;
if (data is Map<String, dynamic>) {
final items = data['items'] as List<dynamic>?;
if (items != null) {
return items
.whereType<Map<String, dynamic>>()
.map(AtomCodeSearchUser.fromJson)
.toList();
}
}
return [];
} else if (statusCode == 401) {
throw const AtomCodeApiException('未授权,请检查 Token 是否正确');
} else if (statusCode == 404) {
throw const AtomCodeApiException('未找到用户');
} else {
throw AtomCodeApiException('HTTP 错误: $statusCode');
}
} on DioException catch (error) {
debugPrint('搜索用户 DioException: ${error.type}, ${error.message}');
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout) {
throw const AtomCodeApiException('请求超时,请检查网络连接');
}
if (error.response?.statusCode == 401) {
throw const AtomCodeApiException('Token 无效或权限不足');
}
throw AtomCodeApiException(error.message ?? '未知网络错误');
} catch (error) {
debugPrint('搜索用户异常: $error');
throw AtomCodeApiException('搜索失败: $error');
}
}
// ====================== 第四步添加的位置 ======================
/// 搜索仓库
Future<List<AtomCodeRepository>> 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>{
'access_token': personalToken,
'q': keyword.trim(),
'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(
'/search/repositories',
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 Map<String, dynamic>) {
final items = data['items'] as List<dynamic>?;
if (items != null) {
return items
.whereType<Map<String, dynamic>>()
.map(AtomCodeRepository.fromJson)
.toList();
}
}
return [];
} else if (statusCode == 401) {
throw const AtomCodeApiException('未授权,请检查 Token 是否正确');
} else if (statusCode == 404) {
throw const AtomCodeApiException('未找到仓库');
} else {
throw AtomCodeApiException('HTTP 错误: $statusCode');
}
} on DioException catch (error) {
debugPrint('搜索仓库 DioException: ${error.type}');
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout) {
throw const AtomCodeApiException('请求超时,请检查网络连接');
}
throw AtomCodeApiException(error.message ?? '未知网络错误');
} catch (error) {
debugPrint('搜索仓库异常: $error');
throw AtomCodeApiException('搜索失败: $error');
}
}
// ====================== 第四步添加结束 ======================
}
/// 搜索用户结果模型
class AtomCodeSearchUser {
const AtomCodeSearchUser({
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 AtomCodeSearchUser.fromJson(Map<String, dynamic> json) {
return AtomCodeSearchUser(
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?,
);
}
}
/// 仓库模型
class AtomCodeRepository {
const AtomCodeRepository({
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 AtomCodeRepository.fromJson(Map<String, dynamic> json) {
return AtomCodeRepository(
fullName: json['full_name'] as String? ?? json['path_with_namespace'] 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?,
stars: _safeInt(json['stargazers_count'] ?? json['star_count']),
forks: _safeInt(json['forks_count'] ?? json['forks']),
watchers: _safeInt(json['watchers_count'] ?? json['watchers']),
ownerLogin: (json['owner'] as Map<String, dynamic>?)?['login'] as String?,
isPrivate: _safeBool(json['private'] ?? json['visibility'] == 'private'),
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;
}
第五步:更新搜索页面实现真实搜索
5.1 修改 search_page.dart
打开 lib/pages/main_navigation/search_page.dart,添加导入:
import 'package:flutter/material.dart';
import '../../core/atomcode_api.dart'; // 添加这行
在 _SearchPageState 类中添加变量:
class _SearchPageState extends State<SearchPage> {
final _client = AtomCodeApiClient(); // 添加 API 客户端
final _keywordController = TextEditingController();
final _tokenController = TextEditingController();
SearchMode _searchMode = SearchMode.user;
bool _tokenObscured = true;
// 添加搜索结果相关变量
bool _isSearching = false;
String? _errorMessage;
List<AtomCodeSearchUser> _userResults = [];
List<AtomCodeRepository> _repoResults = [];
修改 _performSearch 方法:
/// 执行搜索
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 AtomCodeApiException catch (e) {
setState(() {
_errorMessage = e.message;
_isSearching = false;
});
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message)),
);
}
}
定位并替换 _performSearch 方法
找到原代码中 _performSearch 方法的位置(在 _buildTipItem 方法下方),删除原有代码,替换为以下完整的异步方法:
原方法(需要删除):
/// 执行搜索
void _performSearch() {
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;
}
// TODO: 下一章会实现实际的搜索功能
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('准备搜索${_searchMode.label}: $keyword'),
),
);
}
新方法(替换后的完整代码):
/// 执行搜索
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;
// 每次搜索前清空上一次结果,避免混淆
_userResults = [];
_repoResults = [];
});
try {
if (_searchMode == SearchMode.user) {
// 调用用户搜索 API
final users = await _client.searchUsers(
keyword: keyword,
personalToken: token,
perPage: 3, // 预览只显示 3 条
);
setState(() {
_userResults = users;
_isSearching = false;
});
// 空结果提示
if (users.isEmpty) {
if (mounted) { // 防止页面销毁后调用ScaffoldMessenger
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('未找到用户')),
);
}
}
} else {
// 调用仓库搜索 API
final repos = await _client.searchRepositories(
keyword: keyword,
personalToken: token,
perPage: 3,
);
setState(() {
_repoResults = repos;
_isSearching = false;
});
// 空结果提示
if (repos.isEmpty) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('未找到仓库')),
);
}
}
}
} on AtomCodeApiException catch (e) {
// 捕获 API 自定义异常
setState(() {
_errorMessage = e.message;
_isSearching = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message), backgroundColor: Colors.red),
);
}
} catch (e) {
// 捕获其他未知异常
setState(() {
_errorMessage = '搜索出错:${e.toString()}';
_isSearching = false;
});
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('搜索出错:${e.toString()}'), backgroundColor: Colors.red),
);
}
}
}
5.2 添加搜索结果展示
在 build 方法的 Column 中,_buildUsageTips(theme) 之前添加:
// 搜索结果
if (_isSearching)
const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: CircularProgressIndicator(),
),
)
else if (_errorMessage != null)
_buildErrorView(theme)
else if (_searchMode == SearchMode.user && _userResults.isNotEmpty)
_buildUserResults(theme)
else if (_searchMode == SearchMode.repo && _repoResults.isNotEmpty)
_buildRepoResults(theme),
const SizedBox(height: 16),
找到你 build 方法中 Column 的 children 列表,原结构是:
children: [
_buildSearchModeSelector(theme), // 搜索类型切换
const SizedBox(height: 24),
_buildSearchInput(theme), // 搜索输入框
const SizedBox(height: 16),
_buildTokenInput(theme), // Token 输入框
const SizedBox(height: 24),
_buildSearchButton(theme), // 搜索按钮
const SizedBox(height: 32),
_buildUsageTips(theme), // 使用提示
]
需要把 “搜索结果展示代码” 插入到 const SizedBox(height:32), 和 _buildUsageTips(theme), 之间(也就是 _buildUsageTips(theme) 之前)。
插入后的完整结构:
children: [
// 搜索类型切换
_buildSearchModeSelector(theme),
const SizedBox(height: 24),
// 搜索输入框
_buildSearchInput(theme),
const SizedBox(height: 16),
// Token 输入框
_buildTokenInput(theme),
const SizedBox(height: 24),
// 搜索按钮
_buildSearchButton(theme),
const SizedBox(height: 32),
// ====================== 新增的搜索结果展示区域 ======================
// 搜索结果
if (_isSearching)
const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: CircularProgressIndicator(),
),
)
else if (_errorMessage != null)
_buildErrorView(theme)
else if (_searchMode == SearchMode.user && _userResults.isNotEmpty)
_buildUserResults(theme)
else if (_searchMode == SearchMode.repo && _repoResults.isNotEmpty)
_buildRepoResults(theme),
const SizedBox(height: 16),
// ====================== 新增结束 ======================
// 使用提示
_buildUsageTips(theme),
]
添加结果展示方法:
/// 用户搜索结果
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: () {
// TODO: 跳转到完整列表页
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('即将跳转到用户列表')),
);
},
child: const Text('查看全部'),
),
],
),
),
...List.generate(_userResults.length, (index) {
final user = _userResults[index];
return ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(user.avatarUrl),
),
title: Text(user.name ?? user.login),
subtitle: Text('@${user.login}'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// TODO: 跳转到用户详情
},
);
}),
],
),
);
}
/// 仓库搜索结果
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: () {
// TODO: 跳转到完整列表页
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('即将跳转到仓库列表')),
);
},
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,
),
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: 跳转到仓库详情
},
);
}),
],
),
);
}
/// 错误视图
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('重试'),
),
],
),
),
);
}
这三个方法(_buildUserResults、_buildRepoResults、_buildErrorView是 _SearchPageState 类的成员方法,需要添加到该类内部
找到 _SearchPageState 类中现有方法的顺序:
class _SearchPageState extends State<SearchPage> {
// 变量定义...
@override
void dispose() { ... }
@override
Widget build(BuildContext context) { ... }
// 现有方法:
_buildSearchModeSelector(...) { ... }
_buildSearchInput(...) { ... }
_buildTokenInput(...) { ... }
_buildSearchButton(...) { ... }
_buildUsageTips(...) { ... }
_buildTipItem(...) { ... } // ← 这个方法之后
_performSearch(...) { ... } // ← 这个方法之前
}
插入后的完整结构:
在 _buildTipItem 之后、_performSearch 之前,粘贴这三个新方法:
class _SearchPageState extends State<SearchPage> {
// 变量定义(不变)...
@override
void dispose() { ... }
@override
Widget build(BuildContext context) { ... }
// 现有方法(不变)...
Widget _buildSearchModeSelector(ThemeData theme) { ... }
Widget _buildSearchInput(ThemeData theme) { ... }
Widget _buildTokenInput(ThemeData theme) { ... }
Widget _buildSearchButton(ThemeData theme) { ... }
Widget _buildUsageTips(ThemeData theme) { ... }
Widget _buildTipItem(String text) { ... }
// ====================== 新增的3个结果展示方法 ======================
/// 用户搜索结果
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: () {
// TODO: 跳转到完整列表页
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('即将跳转到用户列表')),
);
},
child: const Text('查看全部'),
),
],
),
),
...List.generate(_userResults.length, (index) {
final user = _userResults[index];
return ListTile(
leading: CircleAvatar(
backgroundImage: NetworkImage(user.avatarUrl),
),
title: Text(user.name ?? user.login),
subtitle: Text('@${user.login}'),
trailing: const Icon(Icons.chevron_right),
onTap: () {
// TODO: 跳转到用户详情
},
);
}),
],
),
);
}
/// 仓库搜索结果
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: () {
// TODO: 跳转到完整列表页
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('即将跳转到仓库列表')),
);
},
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,
),
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: 跳转到仓库详情
},
);
}),
],
),
);
}
/// 错误视图
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('重试'),
),
],
),
),
);
}
// ====================== 新增结束 ======================
/// 执行搜索(原有方法,不变)
Future<void> _performSearch() async { ... }
}
第六步:创建用户列表页面
6.1 创建用户列表页文件
创建 lib/pages/user_list_page.dart:
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../core/atomcode_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 = AtomCodeApiClient();
final _refreshController = RefreshController();
List<AtomCodeSearchUser> _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 AtomCodeApiException 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: NetworkImage(user.avatarUrl),
),
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}')),
);
},
),
);
},
),
);
}
}
代码说明:
- 使用
SmartRefresher实现下拉刷新和上拉加载 _currentPage和_hasMore管理分页状态- 完整的状态处理:加载中、错误、空数据、成功
RefreshController需要在dispose中释放
6.2 更新搜索页面跳转
修改 search_page.dart 中的 _buildUserResults 方法:
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserListPage(
keyword: _keywordController.text.trim(),
token: _tokenController.text.trim(),
),
),
);
},
child: const Text('查看全部'),
),
这个修改是替换 _buildUserResults 方法中 “查看全部” 按钮的 onPressed 回调逻辑 —— 把原来的 SnackBar 提示改成跳转到用户列表页(UserListPage)
定位要修改的代码位置:
找到 _buildUserResults 方法中的这段 TextButton 代码:
TextButton(
onPressed: () {
// TODO: 跳转到完整列表页
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('即将跳转到用户列表')),
);
},
child: const Text('查看全部'),
),
替换为新的跳转逻辑
删除原有 onPressed 内的 SnackBar 代码,替换成 Navigator.push 跳转代码:
TextButton(
onPressed: () {
// 替换:从 SnackBar 提示改为跳转到 UserListPage
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => UserListPage(
keyword: _keywordController.text.trim(),
token: _tokenController.text.trim(),
),
),
);
},
child: const Text('查看全部'),
),
同时添加导入:
import '../user_list_page.dart'; // 在文件顶部添加
第七步:创建仓库列表页面
7.1 创建仓库列表页文件
创建 lib/pages/repository_list_page.dart:
import 'package:flutter/material.dart';
import 'package:pull_to_refresh/pull_to_refresh.dart';
import '../core/atomcode_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 = AtomCodeApiClient();
final _refreshController = RefreshController();
List<AtomCodeRepository> _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 AtomCodeApiException 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}'),
],
],
),
],
),
),
);
},
),
);
}
}
7.2 更新搜索页面跳转
修改 search_page.dart 中的 _buildRepoResults 方法:
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepositoryListPage(
keyword: _keywordController.text.trim(),
token: _tokenController.text.trim(),
),
),
);
},
child: const Text('查看全部'),
),
这个修改和之前用户列表的跳转逻辑完全一致,核心是替换 _buildRepoResults 方法中 “查看全部” 按钮的 onPressed 回调 —— 把 SnackBar 提示改成跳转到仓库列表页(RepositoryListPage)
定位要修改的代码位置:
找到 _buildRepoResults 方法里的这段 TextButton 代码:
TextButton(
onPressed: () {
// TODO: 跳转到完整列表页
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('即将跳转到仓库列表')),
);
},
child: const Text('查看全部'),
),
替换为跳转逻辑
删除原有 SnackBar 代码,替换成 Navigator.push 页面跳转代码:
TextButton(
onPressed: () {
// 核心修改:从提示改为跳转到仓库列表页
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => RepositoryListPage(
keyword: _keywordController.text.trim(),
token: _tokenController.text.trim(),
),
),
);
},
child: const Text('查看全部'),
),
同时添加导入:
import '../repository_list_page.dart'; // 在文件顶部添加
第八步:测试搜索功能
8.1 解析并下载项目依赖
flutter pub get

8.2 运行应用
flutter run
8.3 测试步骤
1.获取 Access Token:
访问 https://atomcode.com
登录后进入设置 → 访问令牌
创建新令牌并复制



2.测试用户搜索:
进入"搜索"页面
选择"用户"模式
输入关键字(如:flutter)
输入 Access Token
点击"开始搜索"
验证显示搜索结果
点击"查看全部"进入列表页
测试下拉刷新
滚动到底部测试上拉加载

3.测试仓库搜索:
切换到"仓库"模式
重复上述步骤
项目结构:

补充说明:
本人跟着上面的文章实现,发现代码无法实现搜索功能,如下图:

### 问题描述
用户搜索功能显示"未找到用户",即使输入了正确的关键字和 Token。
### 根本原因
1. API 请求使用了不兼容的 Authorization Bearer Token 头
2. 代码仅支持一种 API 响应格式(对象格式),无法处理数组格式响应
3. 缺少详细的调试日志,难以诊断问题
## 修改文件
### 文件:`lib/core/atomcode_api.dart`
#### 修改 1:简化请求头构建方法(第 28-34 行)
**修改前:**
```dart
Map<String, String> _buildHeaders(String? personalToken) {
return {
if (personalToken != null && personalToken.isNotEmpty)
'Authorization': 'Bearer $personalToken',
};
}
```
**修改后:**
```dart
Map<String, String> _buildHeaders(String? personalToken) {
return {};
}
```
**原因:** AtomCode API 不支持 Bearer Token 认证,Token 通过 `access_token` 查询参数传递。移除不兼容的头部可以避免认证冲突。
#### 修改 2:改进 `searchUsers()` 方法(第 36-99 行)
**变更 2.1:删除请求头参数**
```dart
// 删除前:
options: Options(
headers: _buildHeaders(personalToken),
validateStatus: (status) => status != null && status < 500,
),
// 删除后:
options: Options(
validateStatus: (status) => status != null && status < 500,
),
```
**变更 2.2:改进响应数据解析逻辑**
```dart
// 修改前:仅支持对象格式
if (statusCode == 200) {
final data = response.data;
if (data is Map<String, dynamic>) {
final items = data['items'] as List<dynamic>?;
if (items != null) {
return items
.whereType<Map<String, dynamic>>()
.map(AtomCodeSearchUser.fromJson)
.toList();
}
}
return [];
}
// 修改后:支持数组和对象两种格式
if (statusCode == 200) {
final data = response.data;
if (data is List<dynamic>) {
// 直接返回数组
return data
.whereType<Map<String, dynamic>>()
.map(AtomCodeSearchUser.fromJson)
.toList();
} else if (data is Map<String, dynamic>) {
// 如果是对象,尝试从 items 字段获取
final items = data['items'] as List<dynamic>?;
if (items != null) {
return items
.whereType<Map<String, dynamic>>()
.map(AtomCodeSearchUser.fromJson)
.toList();
}
}
return [];
}
```
**变更 2.3:添加详细调试日志**
```dart
// 新增日志行
debugPrint('搜索用户响应数据: ${response.data}');
// 和
debugPrint('搜索用户错误响应: ${error.response?.data}');
```
**变更 2.4:添加 422 错误处理**
```dart
} else if (statusCode == 422) {
throw const AtomCodeApiException('搜索参数无效');
} else {
```
**原因:**
- 移除无用的请求头提高兼容性
- 支持多种响应格式提高健壮性
- 详细日志便于未来调试
- 处理所有可能的 HTTP 错误状态
#### 修改 3:改进 `searchRepositories()` 方法(第 101-176 行)
**应用了与 `searchUsers()` 完全相同的改进:**
1. **删除了请求头中的 `_buildHeaders(personalToken)`**
2. **改进了响应数据解析**(支持数组和对象格式)
3. **添加了调试日志**
4. **添加了 422 错误处理**
5. **添加了 401 错误处理的补充检查**
具体代码变更与 `searchUsers()` 方法一致。
修改后的gitcode_api.dart完整代码:(修改后即可回到第八步测试)
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
/// AtomCode API 异常类
class AtomCodeApiException implements Exception {
const AtomCodeApiException(this.message);
final String message;
@override
String toString() => 'AtomCodeApiException: $message';
}
/// AtomCode API 客户端
class AtomCodeApiClient {
AtomCodeApiClient({Dio? dio})
: _dio = dio ??
Dio(
BaseOptions(
baseUrl: 'https://api.atomcode.com/api/v5',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
),
);
final Dio _dio;
/// 构建请求头
Map<String, String> _buildHeaders(String? personalToken) {
return {};
}
/// 搜索用户
Future<List<AtomCodeSearchUser>> searchUsers({
required String keyword,
required String personalToken,
int perPage = 10,
int page = 1,
}) async {
try {
debugPrint('搜索用户: $keyword, page: $page');
final response = await _dio.get(
'/search/users',
queryParameters: {
'access_token': personalToken,
'q': keyword.trim(),
'per_page': perPage.clamp(1, 50),
'page': page.clamp(1, 100),
},
options: Options(
validateStatus: (status) => status != null && status < 500,
),
);
final statusCode = response.statusCode ?? 0;
debugPrint('搜索用户响应状态码: $statusCode');
debugPrint('搜索用户响应数据: ${response.data}');
if (statusCode == 200) {
final data = response.data;
if (data is List<dynamic>) {
// 直接返回数组
return data
.whereType<Map<String, dynamic>>()
.map(AtomCodeSearchUser.fromJson)
.toList();
} else if (data is Map<String, dynamic>) {
// 如果是对象,尝试从 items 字段获取
final items = data['items'] as List<dynamic>?;
if (items != null) {
return items
.whereType<Map<String, dynamic>>()
.map(AtomCodeSearchUser.fromJson)
.toList();
}
}
return [];
} else if (statusCode == 401) {
throw const AtomCodeApiException('未授权,请检查 Token 是否正确');
} else if (statusCode == 404) {
throw const AtomCodeApiException('未找到用户');
} else if (statusCode == 422) {
throw const AtomCodeApiException('搜索参数无效');
} else {
throw AtomCodeApiException('HTTP 错误: $statusCode');
}
} on DioException catch (error) {
debugPrint('搜索用户 DioException: ${error.type}, ${error.message}');
debugPrint('搜索用户错误响应: ${error.response?.data}');
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout) {
throw const AtomCodeApiException('请求超时,请检查网络连接');
}
if (error.response?.statusCode == 401) {
throw const AtomCodeApiException('Token 无效或权限不足');
}
throw AtomCodeApiException(error.message ?? '未知网络错误');
} catch (error) {
debugPrint('搜索用户异常: $error');
throw AtomCodeApiException('搜索失败: $error');
}
}
/// 搜索仓库
Future<List<AtomCodeRepository>> 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>{
'access_token': personalToken,
'q': keyword.trim(),
'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(
'/search/repositories',
queryParameters: queryParameters,
options: Options(
validateStatus: (status) => status != null && status < 500,
),
);
final statusCode = response.statusCode ?? 0;
debugPrint('搜索仓库响应状态码: $statusCode');
debugPrint('搜索仓库响应数据: ${response.data}');
if (statusCode == 200) {
final data = response.data;
if (data is List<dynamic>) {
// 直接返回数组
return data
.whereType<Map<String, dynamic>>()
.map(AtomCodeRepository.fromJson)
.toList();
} else if (data is Map<String, dynamic>) {
// 如果是对象,尝试从 items 字段获取
final items = data['items'] as List<dynamic>?;
if (items != null) {
return items
.whereType<Map<String, dynamic>>()
.map(AtomCodeRepository.fromJson)
.toList();
}
}
return [];
} else if (statusCode == 401) {
throw const AtomCodeApiException('未授权,请检查 Token 是否正确');
} else if (statusCode == 404) {
throw const AtomCodeApiException('未找到仓库');
} else if (statusCode == 422) {
throw const AtomCodeApiException('搜索参数无效');
} else {
throw AtomCodeApiException('HTTP 错误: $statusCode');
}
} on DioException catch (error) {
debugPrint('搜索仓库 DioException: ${error.type}');
debugPrint('搜索仓库错误响应: ${error.response?.data}');
if (error.type == DioExceptionType.connectionTimeout ||
error.type == DioExceptionType.receiveTimeout) {
throw const AtomCodeApiException('请求超时,请检查网络连接');
}
if (error.response?.statusCode == 401) {
throw const AtomCodeApiException('Token 无效或权限不足');
}
throw AtomCodeApiException(error.message ?? '未知网络错误');
} catch (error) {
debugPrint('搜索仓库异常: $error');
throw AtomCodeApiException('搜索失败: $error');
}
}
}
/// 搜索用户结果模型
class AtomCodeSearchUser {
const AtomCodeSearchUser({
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 AtomCodeSearchUser.fromJson(Map<String, dynamic> json) {
return AtomCodeSearchUser(
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?,
);
}
}
/// 仓库模型
class AtomCodeRepository {
const AtomCodeRepository({
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 AtomCodeRepository.fromJson(Map<String, dynamic> json) {
return AtomCodeRepository(
fullName: json['full_name'] as String? ?? json['path_with_namespace'] 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?,
stars: _safeInt(json['stargazers_count'] ?? json['star_count']),
forks: _safeInt(json['forks_count'] ?? json['forks']),
watchers: _safeInt(json['watchers_count'] ?? json['watchers']),
ownerLogin: (json['owner'] as Map<String, dynamic>?)?['login'] as String?,
isPrivate: _safeBool(json['private'] ?? json['visibility'] == 'private'),
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;
}
以上为本人跟练的全部过程
更多推荐



所有评论(0)