【开源鸿蒙跨平台Flutter开发】AtomGit Pocket Tool:可视化目录树实现
可视化的目录已成功实现,支持显示完整的仓库文件结构。:渲染README文档(下部主区域)- 待实现。:可视化目录树(上部区域)✅ 本次完成。:实现层级导航与内容展示 - 待实现。:代码高亮 - 待实现。
·
一、阶段目标
1.1 任务来源
在开源社区平台中,代码仓库的详情页面是开发者了解项目结构、浏览源代码的核心入口。本次开发任务的核心目标是构建一个功能完整、体验优秀的代码仓库详情页,特别是要实现能够直观展示项目结构的可视化目录树功能。
项目现状分析:
- 现有仓库列表页面已基本完成
- 用户能够浏览和搜索开源项目
- 缺少进入仓库后的详细内容展示页面
- 需要完整的文件导航和内容预览能力
这次核心任务是实现代码仓库的详情页:
-
核心任务:实现代码仓详情页
- 页面分为上下两大核心区域
- 必须自主处理数据获取、解析和渲染,避免网页套壳
-
支线任务1:可视化目录树(上部区域)✅ 本次完成
- 完整展示仓库的多层级文件目录结构
- 需要递归渲染或使用树形控件展示嵌套文件夹关系
-
支线任务2:渲染README文档(下部主区域)- 待实现
-
支线任务3:实现层级导航与内容展示 - 待实现
-
拓展任务:代码高亮 - 待实现
1.2 本次实现成果
- ✅ 创建
TreeNode数据模型,支持树形结构构建 - ✅ 创建
DirectoryTree可视化组件 - ✅ 重构仓库详情页,实现上下两区域布局
- ✅ 修复私有仓库访问问题(使用
namespacePath)
二、核心实现详解
2.1 Repository 模型优化
关键修复:使用 path 而非 name 字段
AtomGit API 返回的仓库数据中:
path:仓库的实际路径名(如HarmonyosNext)name:显示名称
// lib/models/repository.dart
factory Repository.fromJson(Map<String, dynamic> json) {
final namespace = json['namespace'];
String? namespacePath;
// 从 namespace 获取正确的 owner 路径
if (namespace is Map<String, dynamic>) {
namespacePath = namespace['path']?.toString();
}
// 使用 path 字段作为仓库名(API 调用需要),而不是 name(可能是中文)
final name = json['path']?.toString() ?? json['name']?.toString() ?? '';
return Repository(
// ...
name: name,
namespacePath: namespacePath, // 保存 namespace.path 用于 API 调用
);
}
2.2 导航时使用正确的 owner
// lib/main.dart
void _navigateToRepositoryDetail(Repository repo) {
String owner;
// 优先使用 namespacePath(这是 API 调用需要的正确路径)
if (repo.namespacePath != null && repo.namespacePath!.isNotEmpty) {
owner = repo.namespacePath!;
}
// 从 fullName 中提取 owner(fallback)
else if (repo.fullName.isNotEmpty && repo.fullName.contains('/')) {
final lastSlashIndex = repo.fullName.lastIndexOf('/');
owner = repo.fullName.substring(0, lastSlashIndex);
} else {
owner = repo.ownerName ?? '';
}
Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => RepositoryDetailPage(
owner: owner, // 使用正确的 namespace path
repo: repo.name, // 使用 path 而非显示名
repoId: repo.id,
),
),
);
}
2.3 智能获取目录树
// lib/services/api_service.dart
/// 智能获取目录树:优先使用 file_list API(返回完整文件列表)
static Future<List<TreeNode>> getRepositoryTreeSmart({
required String owner,
required String repo,
int? repoId,
String sha = 'master',
bool recursive = true,
}) async {
// 1. 首先尝试 file_list API(返回完整的文件列表,最可靠)
try {
final paths = await getFileList(owner, repo, refName: sha);
if (paths.isNotEmpty) {
return buildTreeNodesFromPaths(paths);
}
} catch (e) {
print('[DEBUG] file_list API failed: $e');
}
// 2. file_list 失败,尝试 tree API(可能有数量限制)
try {
final result = await getRepositoryTree(owner, repo, sha: sha);
return result;
} catch (e) {
print('[DEBUG] tree API failed: $e');
}
// 3. tree API 也失败,尝试 contents API(递归获取,较慢)
try {
final result = await getRepositoryContentsRecursive(owner, repo, ref: sha);
return result;
} catch (e) {
throw Exception('所有 API 都失败了,无法获取目录树');
}
}
2.4 从文件路径构建目录树
file_list API 只返回文件路径,需要手动构建目录节点:
/// 从文件路径列表构建 TreeNode 列表(包含目录和文件)
static List<TreeNode> buildTreeNodesFromPaths(List<String> paths) {
final nodes = <TreeNode>[];
final addedDirs = <String>{};
for (final path in paths) {
// 添加所有父目录
final parts = path.split('/');
String dirPath = '';
for (int i = 0; i < parts.length - 1; i++) {
dirPath = dirPath.isEmpty ? parts[i] : '$dirPath/${parts[i]}';
if (!addedDirs.contains(dirPath)) {
addedDirs.add(dirPath);
nodes.add(TreeNode(
sha: '',
path: dirPath,
name: parts[i],
type: 'tree', // 目录
));
}
}
// 添加文件
final name = path.contains('/')
? path.substring(path.lastIndexOf('/') + 1)
: path;
nodes.add(TreeNode(
sha: '',
path: path,
name: name,
type: 'blob', // 文件
));
}
return nodes;
}
2.5 TreeNode 数据模型
// lib/models/tree_node.dart
class TreeNode {
final String sha;
final String path; // 完整相对路径
final String name; // 文件/文件夹名称
final String type; // "tree" 或 "blob"
List<TreeNode> children; // 子节点列表
bool isExpanded; // UI 展开状态
bool get isDirectory => type == 'tree';
bool get isFile => type == 'blob';
/// 获取文件扩展名
String? get extension {
if (!isFile) return null;
final dotIndex = name.lastIndexOf('.');
if (dotIndex == -1) return null;
return name.substring(dotIndex + 1).toLowerCase();
}
}
2.6 扁平列表转树形结构
/// 从扁平列表构建树形结构
static List<TreeNode> buildTree(List<TreeNode> flatNodes) {
if (flatNodes.isEmpty) return [];
final Map<String, TreeNode> nodeMap = {};
final List<TreeNode> rootNodes = [];
// 按路径排序,确保父节点在子节点之前
final sortedNodes = List<TreeNode>.from(flatNodes)
..sort((a, b) => a.path.compareTo(b.path));
for (final node in sortedNodes) {
nodeMap[node.path] = node;
final lastSlash = node.path.lastIndexOf('/');
if (lastSlash == -1) {
rootNodes.add(node); // 根节点
} else {
final parentPath = node.path.substring(0, lastSlash);
final parent = nodeMap[parentPath];
if (parent != null) {
parent.children.add(node);
} else {
rootNodes.add(node);
}
}
}
// 排序:文件夹优先,同类型按名称排序
void sortChildren(List<TreeNode> nodes) {
nodes.sort((a, b) {
if (a.isDirectory && !b.isDirectory) return -1;
if (!a.isDirectory && b.isDirectory) return 1;
return a.name.toLowerCase().compareTo(b.name.toLowerCase());
});
for (final node in nodes) {
sortChildren(node.children);
}
}
sortChildren(rootNodes);
return rootNodes;
}
2.7 可视化目录树组件
// lib/widgets/directory_tree.dart
class DirectoryTree extends StatefulWidget {
const DirectoryTree({
required this.nodes,
this.onFileSelected,
this.onFolderSelected,
this.currentPath,
});
final List<TreeNode> nodes;
final void Function(TreeNode)? onFileSelected;
final void Function(TreeNode)? onFolderSelected;
final String? currentPath;
}
class _DirectoryTreeState extends State<DirectoryTree> {
final Set<String> _expandedPaths = {};
void _initExpandedState() {
// 默认不展开任何文件夹,用户需要时自己点击展开
_expandedPaths.clear();
// 如果有 currentPath,展开到该路径
if (widget.currentPath != null) {
final parts = widget.currentPath!.split('/');
String path = '';
for (int i = 0; i < parts.length - 1; i++) {
path = path.isEmpty ? parts[i] : '$path/${parts[i]}';
_expandedPaths.add(path);
}
}
}
Widget _buildTreeNode(TreeNode node, int depth) {
final isExpanded = _expandedPaths.contains(node.path);
return Column(
children: [
_TreeNodeTile(
node: node,
depth: depth,
isExpanded: isExpanded,
onTap: () => _handleNodeTap(node),
),
if (node.isDirectory && isExpanded)
...node.children.map((child) => _buildTreeNode(child, depth + 1)),
],
);
}
}
三、用户体验设计
- 默认折叠:文件夹默认不展开,用户按需点击
- 智能图标:根据文件类型显示不同图标和颜色
四、代码文件变更
4.1 新增文件
| 文件路径 | 说明 |
|---|---|
lib/models/tree_node.dart |
目录树节点数据模型 |
lib/widgets/directory_tree.dart |
可视化目录树组件 |
五、测试结果


支线任务1完成! 可视化的目录已成功实现,支持显示完整的仓库文件结构。这次收获还是挺多的,详情页也多了许多内容,优化了应用的用户体验。明天继续干!
更多推荐


所有评论(0)