GitCode 口袋工具 v1.0.1 更新发布:Flutter 跨平台搜索助手的完整实现
📝 前言
今天很高兴为大家分享 GitCode 口袋工具 的最新版本(v1.0.1)。这是一个基于 Flutter 开发的轻量级 GitCode 搜索工具,支持搜索用户和仓库,提供流畅的分页加载体验。项目专为 OpenHarmony 平台优化。
本文将详细介绍项目的核心功能、技术实现和代码架构,希望能为 Flutter 开发者提供一些参考和启发。

✨ 项目概述
GitCode 口袋工具是一个跨平台的 GitCode 搜索客户端,主要功能包括:
- 🔍 用户搜索:快速搜索 GitCode 用户,查看用户详细信息
- 📦 仓库搜索:搜索代码仓库,查看仓库详情和统计信息
- 📱 分页加载:支持下拉刷新和上拉加载更多
- 🎨 Material Design 3:现代化的 UI 设计
- 🪟 HarmonyOS平台支持:支持 HarmonyOS

🛠️ 技术栈
核心框架
- Flutter 3.6.2+:跨平台 UI 框架
- Dart 3.6.2+:编程语言
主要依赖
- dio ^5.7.0:强大的 HTTP 网络请求库
- pull_to_refresh ^2.0.0:下拉刷新和上拉加载更多组件
- go_router ^14.6.2:声明式路由管理库(已集成,为后续功能扩展准备)
- url_launcher ^6.3.1:用于打开外部链接
🏗️ 项目架构
项目采用清晰的分层架构,主要分为以下几个模块:
lib/
├── core/ # 核心功能模块
│ ├── app_config.dart # 应用配置(Token 等常量)
│ └── gitcode_api.dart # GitCode API 客户端封装
│
├── pages/ # 页面模块
│ ├── main_navigation/ # 主导航相关页面
│ │ ├── intro_page.dart # 首页/介绍页
│ │ ├── search_page.dart # 搜索页面
│ │ └── profile_page.dart # 我的/个人信息页
│ ├── repository_list_page.dart # 仓库列表页(分页加载)
│ └── user_list_page.dart # 用户列表页(分页加载)
│
├── widgets/ # 自定义组件
│ ├── repository_card.dart # 仓库信息卡片组件
│ └── user_card.dart # 用户信息卡片组件
│
└── main.dart # 应用入口,包含主应用和导航栏
💻 核心代码实现
1. API 客户端封装
项目核心是 GitCodeApiClient 类,它封装了所有与 GitCode API 的交互逻辑。
1.1 API 客户端初始化
dart
class GitCodeApiClient {
GitCodeApiClient({Dio? dio})
: _dio = dio ??
Dio(
BaseOptions(
baseUrl: 'https://api.gitcode.com/api/v5',
// 连接和读取阶段都限定为 5 秒,能在弱网场景下快速失败
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 5),
),
);
final Dio _dio;
// ...
}
关键特性:
- 统一配置 API 基础地址
- 设置合理的超时时间(5秒),快速失败机制
- 支持依赖注入,便于测试
1.2 用户搜索实现
dart
Future<List<GitCodeSearchUser>> searchUsers({
required String keyword,
required String personalToken,
int perPage = 10,
int page = 1,
}) async {
final trimmed = keyword.trim();
if (trimmed.isEmpty) {
throw const GitCodeApiException('请输入搜索关键字');
}
final response = await _dio.get<List<dynamic>>(
'/search/users',
queryParameters: <String, dynamic>{
'q': trimmed,
'access_token': personalToken,
// clamp 可以阻止业务层传入不合法的分页参数
'per_page': perPage.clamp(1, 50),
'page': page.clamp(1, 100),
},
options: Options(
headers: _buildHeaders(personalToken),
responseType: ResponseType.json,
validateStatus: (status) => status != null && status < 500,
),
);
final statusCode = response.statusCode ?? 0;
if (statusCode == 401) {
throw const GitCodeApiException('Token 无效或权限不足,无法搜索用户');
}
if (statusCode != 200 || response.data == null) {
throw GitCodeApiException('搜索用户失败 (HTTP $statusCode)');
}
return response.data!
.whereType<Map<String, dynamic>>()
.map(GitCodeSearchUser.fromJson)
.toList();
}
实现亮点:
- 输入验证:自动去除前后空格,防止无效请求
- 参数校验:使用
clamp方法限制分页参数范围(1-50,1-100) - 错误处理:针对 401 未授权错误提供友好提示
- 数据转换:安全的 JSON 解析,使用类型检查和空值处理
1.3 智能用户查找(昵称映射)
这是一个很有特色的功能,当用户输入昵称而非登录名时,系统会自动通过搜索接口映射到真实登录名:
dart
Future<GitCodeUser> fetchUser(
String username, {
String? personalToken,
bool allowSearchFallback = true,
}) async {
final trimmed = username.trim();
// ... 基本验证
try {
final response = await _dio.get<Map<String, dynamic>>(
'/users/${Uri.encodeComponent(trimmed)}',
// ... 请求配置
);
final statusCode = response.statusCode ?? 0;
switch (statusCode) {
case 200:
// 成功返回用户信息
return GitCodeUser.fromJson(data);
case 404:
// 如果允许降级且提供了 Token,尝试通过搜索接口映射
if (allowSearchFallback &&
personalToken != null &&
personalToken.isNotEmpty) {
final fallbackLogin = await _searchLoginByKeyword(
trimmed,
personalToken: personalToken,
);
if (fallbackLogin != null && fallbackLogin != trimmed) {
// 使用真实登录名重新查询,关闭二次降级避免递归
return fetchUser(
fallbackLogin,
personalToken: personalToken,
allowSearchFallback: false,
);
}
}
throw GitCodeApiException(
'未找到用户 $trimmed,若输入的是展示昵称,请改用登录名或携带 token 搜索。',
);
// ... 其他错误处理
}
} catch (error) {
// ... 异常处理
}
}
设计思路:
- 提升用户体验:用户不需要知道确切的登录名
- 防止递归:使用
allowSearchFallback标志避免无限递归 - 优雅降级:API 直接查询失败时,自动尝试搜索接口
2. 分页加载实现
项目使用 pull_to_refresh 包实现流畅的分页加载体验。
2.1 用户列表页实现
dart
class _UserListPageState extends State<UserListPage> {
final RefreshController _refreshController = RefreshController(
initialRefresh: false,
);
final GitCodeApiClient _client = GitCodeApiClient();
List<GitCodeSearchUser> _users = [];
int _currentPage = 1;
final int _perPage = 20;
bool _hasMore = true;
String? _errorMessage;
bool _isLoading = false;
@override
void initState() {
super.initState();
_loadUsers(refresh: true);
}
/// 加载用户列表
Future<void> _loadUsers({bool refresh = false}) async {
if (_isLoading) return;
if (refresh) {
_currentPage = 1;
_hasMore = true;
_users.clear();
}
if (!_hasMore) {
_refreshController.loadNoData();
return;
}
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final users = await _client.searchUsers(
keyword: widget.keyword,
personalToken: widget.token,
perPage: _perPage,
page: _currentPage,
);
setState(() {
if (refresh) {
_users = users;
} else {
_users.addAll(users);
}
// 判断是否还有更多数据
_hasMore = users.length >= _perPage;
_currentPage++;
_errorMessage = null;
_isLoading = false;
});
// 更新刷新控制器状态
if (refresh) {
_refreshController.refreshCompleted();
} else {
if (_hasMore) {
_refreshController.loadComplete();
} else {
_refreshController.loadNoData();
}
}
} on GitCodeApiException catch (e) {
setState(() {
_errorMessage = e.message;
_isLoading = false;
});
if (refresh) {
_refreshController.refreshFailed();
} else {
_refreshController.loadFailed();
}
}
}
/// 下拉刷新
void _onRefresh() {
_loadUsers(refresh: true);
}
/// 上拉加载
void _onLoading() {
_loadUsers(refresh: false);
}
}
核心逻辑:
- 状态管理 :使用
_currentPage、_hasMore、_isLoading等状态变量管理加载状态 - 数据合并:刷新时替换数据,加载更多时追加数据
- 智能判断:根据返回数据量判断是否还有更多数据
- 错误处理:完善的异常捕获和用户提示
2.2 UI 渲染
dart
SmartRefresher(
controller: _refreshController,
enablePullDown: true,
enablePullUp: _hasMore,
header: const ClassicHeader(
refreshingText: '刷新中...',
completeText: '刷新完成',
idleText: '下拉刷新',
releaseText: '释放刷新',
),
footer: ClassicFooter(
loadingText: '加载中...',
noDataText: '没有更多数据了',
idleText: '上拉加载更多',
canLoadingText: '释放加载',
),
onRefresh: _onRefresh,
onLoading: _onLoading,
child: _users.isEmpty && _isLoading
? const Center(child: CircularProgressIndicator())
: _users.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.search_off, size: 64, color: Colors.grey[400]),
const SizedBox(height: 16),
Text('暂无数据', style: TextStyle(color: Colors.grey[600])),
],
),
)
: ListView.builder(
itemCount: _users.length,
padding: const EdgeInsets.symmetric(vertical: 8),
itemBuilder: (context, index) {
return UserCard(
user: _users[index],
onTap: () {
// 处理点击事件
},
);
},
),
)
用户体验优化:
- 加载状态:首次加载显示加载指示器
- 空状态:数据为空时显示友好提示
- 本地化文本:所有提示文本都是中文,符合用户习惯
3. 自定义组件实现
项目设计了两个精美的卡片组件用于展示用户和仓库信息。
3.1 用户卡片组件
dart
class UserCard extends StatelessWidget {
const UserCard({
super.key,
required this.user,
this.onTap,
this.height,
});
final GitCodeSearchUser user;
final VoidCallback? onTap;
final double? height;
// 固定每行的高度常量
static const double _avatarSize = 40.0;
static const double _titleRowHeight = 20.0;
static const double _loginRowHeight = 18.0;
static const double _infoRowHeight = 18.0;
static const double _urlRowHeight = 18.0;
static const double _spacing = 6.0;
static const double _padding = 12.0;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
// 计算实际需要的高度,防止溢出
final calculatedHeight = _padding * 2 +
_avatarSize +
_spacing +
_titleRowHeight +
_spacing +
_loginRowHeight +
_spacing +
_infoRowHeight +
_spacing +
_urlRowHeight +
8.0; // 额外余量防止溢出
final cardHeight = height ?? calculatedHeight;
final displayName = user.name?.isNotEmpty == true ? user.name! : user.login;
return SizedBox(
height: cardHeight,
child: Card(
elevation: 2,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(_padding),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 左侧:头像
CircleAvatar(
radius: _avatarSize / 2,
backgroundImage: NetworkImage(user.avatarUrl),
onBackgroundImageError: (_, __) {},
backgroundColor: theme.colorScheme.surfaceVariant,
child: user.avatarUrl.isEmpty
? Icon(
Icons.person,
size: _avatarSize / 2,
color: theme.colorScheme.onSurfaceVariant,
)
: null,
),
const SizedBox(width: 12),
// 右侧:用户信息
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
// 用户名称(固定高度)
SizedBox(
height: _titleRowHeight,
child: Text(
displayName,
style: theme.textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.bold,
fontSize: 16,
),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
// ... 其他信息行
],
),
),
],
),
),
),
),
);
}
}
设计亮点:
- 固定高度:使用固定高度避免列表滚动时的布局抖动
- 错误处理:头像加载失败时显示默认图标
- 信息展示:清晰的层次结构,重要信息突出显示
- 交互反馈 :使用
InkWell提供点击波纹效果
3.2 仓库卡片组件
dart
class RepositoryCard extends StatelessWidget {
// ... 类似的结构
Color _getLanguageColor(String language) {
// 根据语言返回不同颜色
final colors = <String, Color>{
'Dart': Colors.blue,
'Java': Colors.orange,
'JavaScript': Colors.yellow[700]!,
'Python': Colors.blue[300]!,
'Go': Colors.cyan,
'TypeScript': Colors.blue[800]!,
'C++': Colors.pink,
'C': Colors.grey[800]!,
'Swift': Colors.orange[300]!,
'Kotlin': Colors.purple,
'Rust': Colors.orange[900]!,
};
return colors[language] ?? Colors.grey[600]!;
}
String _formatNumber(int number) {
// 千位格式化,例如 1500 -> 1.5k
if (number >= 1000) {
return '${(number / 1000).toStringAsFixed(1)}k';
}
return number.toString();
}
}
特色功能:
- 语言颜色映射:不同编程语言使用不同颜色标识,提升视觉识别度
- 数字格式化:大数字自动格式化为 k 单位(如 1.5k),提升可读性
- 私有仓库标识:使用锁图标标识私有仓库
4. 搜索页面实现
搜索页面是应用的核心功能页面,支持用户和仓库两种搜索模式。
4.1 搜索状态管理
dart
class _SearchPageState extends State<SearchPage> {
final _keywordController = TextEditingController();
final _tokenController = TextEditingController();
final _formKey = GlobalKey<FormState>();
final _client = GitCodeApiClient();
QueryMode _mode = QueryMode.user;
List<GitCodeSearchUser> _userResults = const [];
List<GitCodeRepository> _repoResults = const [];
String? _errorMessage;
bool _isLoading = false;
bool _obscureToken = true; // Token 显示/隐藏状态
@override
void dispose() {
_keywordController.dispose();
_tokenController.dispose();
super.dispose();
}
}
设计要点:
- 表单验证 :使用
GlobalKey<FormState>进行表单验证 - 资源管理 :在
dispose中释放TextEditingController,防止内存泄漏 - 状态隔离:用户和仓库搜索结果分开存储,避免跨模式数据混淆
4.2 统一搜索入口
dart
Future<void> _performSearch() async {
if (_isLoading) return;
if (!_formKey.currentState!.validate()) return;
FocusScope.of(context).unfocus(); // 收起键盘
setState(() {
_isLoading = true;
_errorMessage = null;
_userResults = const [];
_repoResults = const [];
});
final token = _tokenController.text.trim();
try {
// 根据当前模式执行对应的 API 请求
if (_mode == QueryMode.user) {
final users = await _client.searchUsers(
keyword: _keywordController.text,
personalToken: token,
perPage: 8,
);
setState(() {
_userResults = users;
});
} else {
final repos = await _client.searchRepositories(
keyword: _keywordController.text,
personalToken: token,
perPage: 8,
);
setState(() {
_repoResults = repos;
});
}
} on GitCodeApiException catch (error) {
setState(() {
_errorMessage = error.message;
});
} catch (error) {
setState(() {
_errorMessage = '未知错误:$error';
});
} finally {
if (mounted) {
setState(() {
_isLoading = false;
});
}
}
}
用户体验优化:
- 防止重复请求 :通过
_isLoading标志防止并发请求 - 表单验证:搜索前验证输入合法性
- 自动收起键盘:搜索时自动收起键盘,提升体验
- 错误处理:区分 API 异常和未知异常,提供相应提示
4.3 Token 安全输入
dart
TextFormField(
controller: _tokenController,
decoration: InputDecoration(
labelText: 'Access Token',
hintText: 'GitCode 个人设置 > Access Token',
prefixIcon: const Icon(Icons.vpn_key_outlined),
suffixIcon: IconButton(
icon: Icon(
_obscureToken
? Icons.visibility_off_outlined
: Icons.visibility_outlined,
),
onPressed: () {
setState(() {
_obscureToken = !_obscureToken;
});
},
),
),
obscureText: _obscureToken, // 支持显示/隐藏切换
validator: (value) {
if (value == null || value.trim().isEmpty) {
return '搜索接口需提供 access token';
}
return null;
},
),
安全特性:
- 默认隐藏:Token 默认隐藏,保护敏感信息
- 一键切换:点击眼睛图标快速切换显示/隐藏状态
- 输入验证:表单验证确保 Token 不为空
5. 主导航实现
应用使用底部导航栏管理三个主要页面,使用 IndexedStack 保持页面状态。
dart
class MainNavigationPage extends StatefulWidget {
const MainNavigationPage({super.key});
@override
State<MainNavigationPage> createState() => _MainNavigationPageState();
}
class _MainNavigationPageState extends State<MainNavigationPage> {
int _currentIndex = 0;
final List<Widget> _pages = const [
IntroPage(),
SearchPage(),
ProfilePage(),
];
@override
Widget build(BuildContext context) {
return Scaffold(
body: IndexedStack(
index: _currentIndex,
children: _pages,
),
bottomNavigationBar: NavigationBar(
selectedIndex: _currentIndex,
onDestinationSelected: (int index) {
setState(() {
_currentIndex = index;
});
},
destinations: const [
NavigationDestination(
icon: Icon(Icons.home_outlined),
selectedIcon: Icon(Icons.home),
label: '首页',
),
NavigationDestination(
icon: Icon(Icons.search_outlined),
selectedIcon: Icon(Icons.search),
label: '搜索',
),
NavigationDestination(
icon: Icon(Icons.person_outline),
selectedIcon: Icon(Icons.person),
label: '我的',
),
],
),
);
}
}
设计优势:
- 状态保持 :使用
IndexedStack保持所有页面状态,切换时不重建页面 - Material 3 :使用
NavigationBar组件,符合 Material Design 3 规范 - 图标切换:选中和未选中状态使用不同图标,视觉反馈清晰
🎯 关键技术点总结
1. 错误处理机制
项目实现了完善的错误处理机制:
- 网络超时:连接和读取超时均为 5 秒,快速失败并提示用户
- Token 验证:401 错误时提示 Token 无效或权限不足
- 智能降级:用户搜索 404 时,自动尝试通过搜索接口映射昵称到登录名
- 友好提示:所有错误信息都经过本地化处理,用户友好
2. 性能优化
- 固定高度卡片:避免列表滚动时的布局抖动
- IndexedStack:保持页面状态,避免重复构建
- 懒加载 :使用
ListView.builder实现列表项的懒加载 - 请求去重:通过状态标志防止重复请求
3. 用户体验优化
- 加载状态:所有异步操作都有明确的加载指示
- 空状态处理:数据为空时显示友好提示
- 输入验证:表单输入实时验证,防止无效请求
- Token 安全:支持 Token 输入框显示/隐藏切换
📦 使用指南
快速开始
- 克隆项目
bash
git clone https://gitcode.com/byyixuan/gitcode_pocket_tool.git
cd gitcode_pocket_tool
- 安装依赖
bash
flutter pub get
- 配置 Access Token
在 lib/core/app_config.dart 文件中配置你的 GitCode Access Token:
dart
static const demoToken = '<你的 GitCode 个人访问令牌>';
- 运行项目
bash
# OpenHarmony 平台
flutter run
# Windows 平台
flutter run -d windows
获取 Access Token
- 登录 GitCode
- 进入 个人设置 > Access Token
- 创建新的访问令牌
- 复制令牌并粘贴到应用的 Token 输入框
🚀 后续计划
近期计划
- 支持更多搜索过滤条件(语言、排序方式等)
- 添加用户详情页面(展示完整用户信息)
- 添加仓库详情页面(展示完整仓库信息)
- 支持收藏功能(本地存储)
- 支持搜索历史记录
- 暗色模式支持
长期计划
- 支持多语言国际化
- 添加数据缓存机制
- 支持离线浏览
- 添加分享功能
- 支持自定义主题
📝 总结
GitCode 口袋工具 v1.0.1 版本实现了一个功能完整、体验流畅的 GitCode 搜索客户端。项目采用了清晰的架构设计,完善的错误处理机制,以及优秀的用户体验优化。
核心亮点:
- ✅ 完善的 API 封装,支持智能降级和错误处理
- ✅ 流畅的分页加载体验,支持下拉刷新和上拉加载
- ✅ 精美的 UI 组件设计,固定高度避免布局抖动
- ✅ 完善的错误处理和用户提示
- ✅ 清晰的代码架构,易于维护和扩展
如果你对这个项目感兴趣,欢迎 Star 和 Fork!也欢迎提交 Issue 和 Pull Request,共同完善这个项目。
🔗 相关链接
- 项目地址:https://gitcode.com/byyixuan/gitcode_pocket_tool
- GitCode 主页:@byyixuan
- CSDN 博客:https://blog.csdn.net/2301_80035882
⭐ 如果这个项目对你有帮助,请给个 Star 支持一下!