鴻蒙應用開發進階:Pocket Tool 分支協作模塊的設計與落地
一、階段目標與實現總結
1.1 項目迭代背景
此前已完成倉庫詳情查看、基礎搜索及個人倉庫列表等核心功能,實現了數據的流暢加載與展示。隨着用户使用場景的深入,單一的倉庫瀏覽已無法滿足協作開發需求,本次迭代重點實現分支管理核心功能,支撐多人協作場景。
最終實現:
"倉庫詳情"頁面:新增分支列表入口及管理面板
"協作中心"頁面:新增分支創建、切換、刪除及合併請求發起功能
二、核心實現詳解
2.1 Branch模型的狀態與關聯處理
位置: lib/models/branch.dart
factory Branch.fromJson(Map<String, dynamic> json, String repoFullName) {
// 解析分支基礎信息
final name = json['name']?.toString() ?? '';
final commit = json['commit'] as Map<String, dynamic>;
final commitSha = commit['sha']?.toString() ?? '';
final commitMessage = commit['commit']['message']?.toString() ?? '無提交信息';
// 處理分支保護狀態(核心協作屬性)
bool isProtected = false;
if (json.containsKey('protection')) {
isProtected = json['protection']['enabled'] as bool? ?? false;
}
// 關聯倉庫信息(用於後續API請求)
final List<String> repoParts = repoFullName.split('/');
String owner = '';
String repoName = '';
if (repoParts.length >= 2) {
owner = repoParts.sublist(0, repoParts.length - 1).join('/');
repoName = repoParts.last;
}
// 解析提交時間
final updatedAt = _parseDateTime(commit['commit']['committer']['date']);
return Branch(
name: name,
repoOwner: owner,
repoName: repoName,
commitSha: commitSha,
commitMessage: commitMessage,
isProtected: isProtected,
updatedAt: updatedAt,
isDefault: json['default'] as bool? ?? false
);
}
// 輔助解析方法
static DateTime? _parseDateTime(String? dateStr) {
if (dateStr == null) return null;
try {
return DateTime.parse(dateStr);
} catch (_) {
return null;
}
}
代碼説明:
關聯關係設計:
通過倉庫全名拆分獲取所有者和倉庫名,建立分支與倉庫的強關聯,為後續分支操作API提供必要參數。支持嵌套命名空間場景(如gh_mirrors/op/OpenManus)的拆分處理。
協作屬性強化:
重點解析分支保護狀態(isProtected),為後續刪除/合併操作提供權限判斷依據;默認分支標識(isDefault)用於界面突出顯示,提升用户識別效率。
2.2 分支列表與操作面板實現
位置: lib/pages/repository_detail_page.dart - _BranchListState
// 分支列表構建(支持下拉刷新)
Widget _buildBranchList() {
return RefreshIndicator(
onRefresh: _fetchBranches,
child: ListView.separated(
padding: const EdgeInsets.all(16),
itemCount: _branches.length,
separatorBuilder: (context, index) => const Divider(height: 8),
itemBuilder: (context, index) {
final branch = _branches[index];
return ListTile(
leading: branch.isDefault
? const Icon(Icons.star, color: Colors.amber)
: const Icon(Icons.code_branch),
title: Text(branch.name),
subtitle: Text(
'${branch.commitSha.substring(0, 7)} · ${branch.commitMessage}',
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
trailing: _buildBranchActionButtons(branch),
onTap: () => _switchBranch(branch),
);
},
),
);
}
// 分支操作按鈕(根據保護狀態動態顯示)
Widget _buildBranchActionButtons(Branch branch) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.merge),
onPressed: () => _showMergeRequestDialog(branch),
tooltip: '發起合併請求',
),
IconButton(
icon: const Icon(Icons.delete),
onPressed: branch.isProtected || branch.isDefault
? null // 保護分支/默認分支禁用刪除
: () => _confirmDeleteBranch(branch),
tooltip: branch.isProtected ? '分支已保護' : '刪除分支',
disabledColor: Colors.grey[300],
),
],
);
}
2.3 分支管理API服務層
位置: lib/services/api_service.dart
/// 獲取倉庫分支列表
static Future<List<Map<String, dynamic>>> getRepoBranches(String owner, String repo) async {
final url = Uri.parse('$baseUrl/repos/$owner/$repo/branches');
final response = await _get(url);
if (response.statusCode == 200) {
final dynamic data = json.decode(response.body);
if (data is List) {
return data.cast<Map<String, dynamic>>();
}
throw Exception('Unexpected branches list format.');
}
throw Exception('Failed to get branches: ${response.statusCode} - ${response.body}');
}
/// 創建新分支
static Future<Map<String, dynamic>> createBranch(String owner, String repo, String branchName, String baseSha) async {
final url = Uri.parse('$baseUrl/repos/$owner/$repo/branches');
final body = json.encode({
'branch': branchName,
'ref': baseSha // 基於指定提交創建分支
});
final response = await _post(url, body: body);
if (response.statusCode == 201) {
return json.decode(response.body) as Map<String, dynamic>;
}
throw Exception('Failed to create branch: ${response.statusCode} - ${response.body}');
}
/// 刪除分支
static Future<void> deleteBranch(String owner, String repo, String branchName) async {
final url = Uri.parse('$baseUrl/repos/$owner/$repo/branches/$branchName');
final response = await _delete(url);
if (response.statusCode != 204) {
throw Exception('Failed to delete branch: ${response.statusCode} - ${response.body}');
}
}
代碼説明:
完整生命週期覆蓋:
提供分支查詢、創建、刪除全流程API封裝,支持基於指定提交SHA創建分支,滿足精準分支管理需求。
權限兼容處理:
通過HTTP狀態碼精準判斷操作結果,針對保護分支刪除等非法操作返回明確錯誤信息,便於前端提示。
2.4 合併請求核心功能
位置: lib/pages/merge_request_page.dart
合併請求作為分支協作的核心環節,實現以下關鍵功能:
- 分支選擇器: 支持源分支與目標分支的聯動選擇,默認選中當前分支作為源分支,主分支作為目標分支
- 衝突檢測: 提交前調用API預檢測分支衝突,衝突時顯示衝突文件列表及解決方案提示
- 請求詳情: 支持填寫合併標題、描述、指定審核人,關聯相關Issue
- 狀態跟蹤: 實時展示合併請求狀態(待審核、已通過、已拒絕、合併中、已合併)
三、體驗優化
- 操作反饋強化:分支創建/刪除成功後顯示頂部Toast提示,3秒後自動消失
- 衝突檢測結果以高亮卡片展示,提供"查看衝突文件"快捷入口
- 長時間操作(如分支創建)顯示加載對話框,防止重複提交
- 列表優化:默認分支添加星標標識,保護分支添加盾牌圖標,提升視覺識別效率
- 分支列表按更新時間倒序排列,最新操作的分支優先展示
- 支持分支搜索過濾,輸入關鍵詞實時匹配分支名稱
- 異常處理:網絡錯誤時顯示重試按鈕,點擊可重新執行操作
- 權限不足操作時顯示明確提示,並提供"申請權限"跳轉入口
- 刪除分支時增加二次確認對話框,防止誤操作
四、後續優化方向
- 分支可視化:添加分支歷史時間線,直觀展示分支創建、合併、刪除記錄
- 實現分支網絡拓撲圖,展示多分支間的關聯關係
- 協作效率提升:支持分支權限精細化管理(如指定人員可合併到主分支)
- 添加合併請求模板,規範提交內容
- 實現合併請求審核通知(站內信+推送)
- 離線能力增強:支持分支列表離線緩存,無網絡時可查看歷史分支信息
- 離線操作記錄本地存儲,網絡恢復後自動同步
五、測試結果
| 測試場景 | 測試用例 | 測試結果 |
|---|---|---|
| 分支管理 | 創建/刪除普通分支、默認分支、保護分支 | 通過(保護分支/默認分支刪除已禁用) |
| 合併請求 | 無衝突合併、有衝突檢測、指定審核人 | 通過(衝突檢測準確,狀態跟蹤正常) |
| 異常場景 | 網絡中斷、權限不足、重複創建同名分支 | 通過(錯誤提示清晰,支持重試) |
| 性能測試 | 100+分支列表加載、批量刪除分支 | 通過(加載耗時<1s,無卡頓) |
六、總結
本次迭代完成了分支管理全流程功能開發,核心實現了分支的創建、查詢、刪除及合併請求發起與跟蹤,通過精細化的權限控制和直觀的操作反饋,顯著提升了協作開發體驗。從單一的倉庫瀏覽工具,向輕量級協作平台邁出了關鍵一步。
後續將重點優化分支可視化和審核流程自動化,進一步提升團隊協作效率。深夜的代碼終於有了成果,期待明天用户的反饋!