欢迎大家加入[开源鸿蒙跨平台开发者社区](https://openharmonycrossplatform.csdn.net),一起共建开源鸿蒙跨平台生态。

目录

0. 本篇目标

1. 功能拓展 1:分支/标签切换(Branch Switcher)

1.1 Service 增加 branches/tags 接口

1.2 Provider:把 ref(branch) 变成全局状态

1.3 UI:顶部 BranchSwitchBar

2. 功能拓展 2:仓库内搜索(文件名 / 路径)

2.1 Provider:搜索结果

2.2 UI:SearchBar + ResultList

3. 深度优化 1:目录树“可视节点扁平化 visibleNodes”

3.1 Model:TreeItem + 展开状态

3.2 buildTree(扁平 → 层级)

3.3 visibleNodes 计算与展开/收起

3.4 UI:用 ListView 渲染 visibleNodes

4. 深度优化 2:三层缓存 + LRU(内存)

在 Service 里接入缓存

5. 深度优化 3:并发取消 / 结果防乱序

6. 深度优化 4:代码高亮 isolate(大文件不卡 UI)

7. RepoDetailPage 组合(把新功能接进去)

8. 小结



「D12-D16」功能拓展与深度优化3(含代码)

0. 本篇目标

前两篇已经完成:

  • recursive 拉全树、本地建树与目录树渲染

  • README 原生 Markdown 渲染

  • 文件页 base64 解码 + 高亮

  • 缓存 / 虚拟列表 / 并发取消等优化点 (CSDN博客)

本篇继续做两件事:

  1. 功能补齐:分支切换、仓库搜索、多类型预览、收藏/最近、离线只读模式。

  2. 深度优化落地:扁平可视节点(visibleNodes)、三层 LRU 缓存、请求 token/取消、高亮 isolate。


1. 功能拓展 1:分支/标签切换(Branch Switcher)

1.1 Service 增加 branches/tags 接口

// services/gitcode_api_service.dart (新增)
class GitCodeApiService {
  // ...前两篇已有代码

  Future<List<String>> fetchBranches({
    required String owner,
    required String repo,
    int page = 1,
    int perPage = 50,
  }) async {
    final resp = await _dio.get(
      '/repos/$owner/$repo/branches',
      queryParameters: {'page': page, 'per_page': perPage},
    );
    final list = (resp.data as List?) ?? [];
    return list.map((e) => e['name'] as String).toList();
  }

  Future<List<String>> fetchTags({
    required String owner,
    required String repo,
    int page = 1,
    int perPage = 50,
  }) async {
    final resp = await _dio.get(
      '/repos/$owner/$repo/tags',
      queryParameters: {'page': page, 'per_page': perPage},
    );
    final list = (resp.data as List?) ?? [];
    return list.map((e) => e['name'] as String).toList();
  }
}

1.2 Provider:把 ref(branch) 变成全局状态

前两篇 RepoId 带 branch,这里让 branch 可被 UI 改变,自动刷新 tree/readme/file providers。 (CSDN博客)

// providers/repo_detail_providers.dart (新增/改造)
final currentBranchProvider = StateProvider<String>((ref) => 'master');

final branchesProvider = FutureProvider.family<List<String>, RepoId>((ref, repoId) {
  final api = ref.read(gitCodeApiProvider);
  return api.fetchBranches(owner: repoId.owner, repo: repoId.repo);
});

final tagsProvider = FutureProvider.family<List<String>, RepoId>((ref, repoId) {
  final api = ref.read(gitCodeApiProvider);
  return api.fetchTags(owner: repoId.owner, repo: repoId.repo);
});

// 让 RepoId 不再自带 branch,branch 从 currentBranchProvider 取
class RepoId {
  final String owner;
  final String repo;
  const RepoId(this.owner, this.repo);
}

1.3 UI:顶部 BranchSwitchBar

// widgets/branch_switch_bar.dart
class BranchSwitchBar extends ConsumerWidget {
  final RepoId repoId;
  const BranchSwitchBar({super.key, required this.repoId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final curBranch = ref.watch(currentBranchProvider);
    final branchesAsync = ref.watch(branchesProvider(repoId));

    return branchesAsync.when(
      data: (branches) {
        return Row(
          children: [
            const Icon(Icons.alt_route, size: 18),
            const SizedBox(width: 8),
            DropdownButton<String>(
              value: curBranch,
              items: branches
                  .map((b) => DropdownMenuItem(value: b, child: Text(b)))
                  .toList(),
              onChanged: (b) {
                if (b == null) return;
                ref.read(currentBranchProvider.notifier).state = b;

                // 关键:刷新依赖 branch 的数据
                ref.invalidate(repoTreeProvider);
                ref.invalidate(readmeProvider);
                ref.invalidate(currentPathProvider);
              },
            ),
          ],
        );
      },
      loading: () => const SizedBox(height: 36, child: Center(child: CircularProgressIndicator())),
      error: (e, st) => Text('分支加载失败: $e'),
    );
  }
}

2. 功能拓展 2:仓库内搜索(文件名 / 路径)

基于前两篇“recursive=1 拉全树+本地索引”的建议做本地搜索。 (CSDN博客)

2.1 Provider:搜索结果

// providers/repo_search_providers.dart
final searchQueryProvider = StateProvider<String>((ref) => '');

final searchResultProvider =
    Provider.family<List<RepoTreeNode>, RepoId>((ref, repoId) {
  final q = ref.watch(searchQueryProvider).trim().toLowerCase();
  if (q.isEmpty) return const [];

  final treeAsync = ref.watch(repoTreeProvider(repoId));
  return treeAsync.maybeWhen(
    data: (nodes) {
      return nodes.where((n) {
        final p = n.path.toLowerCase();
        return p.contains(q); // path/name 命中
      }).toList();
    },
    orElse: () => const [],
  );
});

2.2 UI:SearchBar + ResultList

// widgets/repo_search_bar.dart
class RepoSearchBar extends ConsumerWidget {
  final RepoId repoId;
  const RepoSearchBar({super.key, required this.repoId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final results = ref.watch(searchResultProvider(repoId));

    return Column(
      children: [
        TextField(
          decoration: const InputDecoration(
            hintText: '搜索文件/路径',
            prefixIcon: Icon(Icons.search),
          ),
          onChanged: (v) => ref.read(searchQueryProvider.notifier).state = v,
        ),
        if (results.isNotEmpty)
          SizedBox(
            height: 220,
            child: ListView.builder(
              itemCount: results.length,
              itemBuilder: (_, i) {
                final n = results[i];
                return ListTile(
                  dense: true,
                  leading: Icon(n.isDir ? Icons.folder : Icons.insert_drive_file),
                  title: Text(n.path, maxLines: 1, overflow: TextOverflow.ellipsis),
                  onTap: () {
                    // 跳转到该文件/目录
                    if (n.isDir) {
                      ref.read(currentPathProvider.notifier).state = n.path;
                    } else {
                      Navigator.push(
                        context,
                        MaterialPageRoute(
                          builder: (_) => FileContentPage(
                            repoId: repoId,
                            path: n.path,
                            sha: n.sha,
                          ),
                        ),
                      );
                    }
                  },
                );
              },
            ),
          )
      ],
    );
  }
}

3. 深度优化 1:目录树“可视节点扁平化 visibleNodes”

前两篇提到“递归渲染简单但大仓库建议 visibleNodes 列表”。这里把它完整落地。 (CSDN博客)

3.1 Model:TreeItem + 展开状态

// models/tree_item.dart
class TreeItem {
  final String name;
  final String path;
  final bool isDir;
  final String sha;
  final int depth;
  final List<TreeItem> children;
  final bool expanded;

  TreeItem({
    required this.name,
    required this.path,
    required this.isDir,
    required this.sha,
    required this.depth,
    this.children = const [],
    this.expanded = false,
  });

  TreeItem copyWith({
    List<TreeItem>? children,
    bool? expanded,
  }) {
    return TreeItem(
      name: name,
      path: path,
      isDir: isDir,
      sha: sha,
      depth: depth,
      children: children ?? this.children,
      expanded: expanded ?? this.expanded,
    );
  }
}

3.2 buildTree(扁平 → 层级)

// utils/build_tree.dart
List<TreeItem> buildTree(List<RepoTreeNode> nodes) {
  final root = <String, TreeItem>{};

  TreeItem ensureNode(String path, bool isDir, String sha, int depth) {
    return root.putIfAbsent(path, () {
      final name = path.split('/').last;
      return TreeItem(
        name: name,
        path: path,
        isDir: isDir,
        sha: sha,
        depth: depth,
        children: [],
      );
    });
  }

  for (final n in nodes) {
    final parts = n.path.split('/');
    String curPath = '';
    for (int i = 0; i < parts.length; i++) {
      curPath = curPath.isEmpty ? parts[i] : '$curPath/${parts[i]}';
      final isLeaf = i == parts.length - 1;
      ensureNode(
        curPath,
        isLeaf ? n.isDir : true,
        isLeaf ? n.sha : '',
        i,
      );
    }
  }

  // 组装父子
  final map = Map<String, TreeItem>.from(root);
  final childrenMap = <String, List<TreeItem>>{};
  for (final item in map.values) {
    final parent = item.path.contains('/')
        ? item.path.substring(0, item.path.lastIndexOf('/'))
        : '';
    childrenMap.putIfAbsent(parent, () => []).add(item);
  }

  TreeItem attach(TreeItem item) {
    final kids = (childrenMap[item.path] ?? [])
      ..sort((a, b) => a.name.compareTo(b.name));
    return item.copyWith(children: kids.map(attach).toList());
  }

  final top = (childrenMap[''] ?? [])..sort((a, b) => a.name.compareTo(b.name));
  return top.map(attach).toList();
}

3.3 visibleNodes 计算与展开/收起

// providers/tree_visible_provider.dart
final treeRootProvider =
    Provider.family<List<TreeItem>, RepoId>((ref, repoId) {
  final nodesAsync = ref.watch(repoTreeProvider(repoId));
  return nodesAsync.maybeWhen(
    data: (nodes) => buildTree(nodes),
    orElse: () => const [],
  );
});

final visibleNodesProvider =
    StateNotifierProvider.family<VisibleNodesNotifier, List<TreeItem>, RepoId>(
        (ref, repoId) {
  final roots = ref.watch(treeRootProvider(repoId));
  return VisibleNodesNotifier(roots);
});

class VisibleNodesNotifier extends StateNotifier<List<TreeItem>> {
  List<TreeItem> roots;
  VisibleNodesNotifier(this.roots) : super(_calcVisible(roots));

  static List<TreeItem> _calcVisible(List<TreeItem> roots) {
    final out = <TreeItem>[];
    void dfs(TreeItem n) {
      out.add(n);
      if (n.isDir && n.expanded) {
        for (final c in n.children) dfs(c);
      }
    }
    for (final r in roots) dfs(r);
    return out;
  }

  void toggle(TreeItem node) {
    TreeItem rebuild(TreeItem n) {
      if (n.path == node.path) return n.copyWith(expanded: !n.expanded);
      if (n.children.isEmpty) return n;
      return n.copyWith(children: n.children.map(rebuild).toList());
    }
    roots = roots.map(rebuild).toList();
    state = _calcVisible(roots);
  }
}

3.4 UI:用 ListView 渲染 visibleNodes

// widgets/repo_tree_flat_view.dart
class RepoTreeFlatView extends ConsumerWidget {
  final RepoId repoId;
  const RepoTreeFlatView({super.key, required this.repoId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final visible = ref.watch(visibleNodesProvider(repoId));

    return ListView.builder(
      itemCount: visible.length,
      itemBuilder: (_, i) {
        final n = visible[i];
        return InkWell(
          onTap: () {
            if (n.isDir) {
              ref.read(visibleNodesProvider(repoId).notifier).toggle(n);
              ref.read(currentPathProvider.notifier).state = n.path;
            } else {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (_) => FileContentPage(
                    repoId: repoId,
                    path: n.path,
                    sha: n.sha,
                  ),
                ),
              );
            }
          },
          child: Padding(
            padding: EdgeInsets.only(left: 12.0 * n.depth, right: 8),
            child: Row(
              children: [
                Icon(
                  n.isDir ? Icons.folder : Icons.insert_drive_file,
                  size: 18,
                ),
                const SizedBox(width: 6),
                Expanded(child: Text(n.name)),
                if (n.isDir)
                  Icon(n.expanded ? Icons.expand_less : Icons.expand_more),
              ],
            ),
          ),
        );
      },
    );
  }
}

4. 深度优化 2:三层缓存 + LRU(内存)

前两篇提到“tree/readme/path cache + LRU”。这里实现一个可复用 CacheManager。 (CSDN博客)

// infra/cache_manager.dart
class LruCache<K, V> {
  final int capacity;
  final _map = LinkedHashMap<K, V>();

  LruCache({required this.capacity});

  V? get(K key) {
    if (!_map.containsKey(key)) return null;
    final v = _map.remove(key)!;
    _map[key] = v; // recent
    return v;
  }

  void put(K key, V value) {
    if (_map.containsKey(key)) _map.remove(key);
    _map[key] = value;
    if (_map.length > capacity) {
      _map.remove(_map.keys.first);
    }
  }
}

class CacheManager {
  static final CacheManager I = CacheManager._();
  CacheManager._();

  final treeCache = LruCache<String, List<RepoTreeNode>>(capacity: 10);
  final fileCache = LruCache<String, String>(capacity: 50);
  final readmeCache = LruCache<String, ReadmeData>(capacity: 20);

  String treeKey(RepoId id, String branch) => '${id.owner}/${id.repo}@$branch';
  String fileKey(RepoId id, String branch, String path, String sha) =>
      '${id.owner}/${id.repo}@$branch:$path#$sha';
}

在 Service 里接入缓存

// services/gitcode_api_service.dart (改造 fetchRepoTree / fetchBlobText / fetchReadme)
Future<List<RepoTreeNode>> fetchRepoTreeCached({
  required RepoId repoId,
  required String branch,
}) async {
  final key = CacheManager.I.treeKey(repoId, branch);
  final cached = CacheManager.I.treeCache.get(key);
  if (cached != null) return cached;

  final nodes = await fetchRepoTree(
    owner: repoId.owner,
    repo: repoId.repo,
    sha: branch,
    recursive: true,
  );
  CacheManager.I.treeCache.put(key, nodes);
  return nodes;
}

Future<String> fetchBlobTextCached({
  required RepoId repoId,
  required String branch,
  required String path,
  required String sha,
}) async {
  final key = CacheManager.I.fileKey(repoId, branch, path, sha);
  final cached = CacheManager.I.fileCache.get(key);
  if (cached != null) return cached;

  final text = await fetchBlobText(owner: repoId.owner, repo: repoId.repo, sha: sha);
  CacheManager.I.fileCache.put(key, text);
  return text;
}

5. 深度优化 3:并发取消 / 结果防乱序

前两篇提醒“快速切目录要取消旧请求”。这里给 RequestVersion 落地。 (CSDN博客)

// infra/request_guard.dart
class RequestGuard {
  int _version = 0;
  int next() => ++_version;
  bool isLatest(int v) => v == _version;
}

final requestGuardProvider = Provider((_) => RequestGuard());

使用示例(以 README 为例):

// providers/repo_detail_providers.dart (readmeProvider 改造)
final readmeProvider = FutureProvider.family<ReadmeData?, RepoId>((ref, repoId) async {
  final api = ref.read(gitCodeApiProvider);
  final branch = ref.read(currentBranchProvider);
  final guard = ref.read(requestGuardProvider);

  final v = guard.next(); // 发请求前递增版本
  final data = await api.fetchReadme(
    owner: repoId.owner,
    repo: repoId.repo,
    ref: branch,
  );

  if (!guard.isLatest(v)) return null; // 丢弃过期结果
  return data;
});

6. 深度优化 4:代码高亮 isolate(大文件不卡 UI)

前两篇用 highlight token 做原生渲染,但大文件会卡。这里把 tokenize 放到 isolate。 (CSDN博客)

// infra/highlight_worker.dart
import 'dart:isolate';
import 'package:highlight/highlight.dart' as hi;
import 'package:highlight/languages/dart.dart' as dart;

class HighlightRequest {
  final String code;
  final String language;
  HighlightRequest(this.code, this.language);
}

Future<List<hi.Node>> highlightInIsolate(HighlightRequest req) async {
  final rp = ReceivePort();
  await Isolate.spawn(_entry, [rp.sendPort, req]);
  return await rp.first as List<hi.Node>;
}

void _entry(List args) {
  final SendPort send = args[0];
  final HighlightRequest req = args[1];

  hi.highlight.registerLanguage('dart', dart.dart);
  final res = hi.highlight.parse(req.code, language: req.language, autoDetection: false);
  send.send(res.nodes ?? []);
}

文件页里使用:

// pages/file_content_page.dart (片段)
final code = await api.fetchBlobTextCached(
  repoId: repoId,
  branch: branch,
  path: path,
  sha: sha,
);

final nodes = await highlightInIsolate(HighlightRequest(code, language));
// nodes -> TextSpan 渲染(沿用你前两篇高亮渲染逻辑)

同时加阈值:

if (code.length > 300 * 1024) {
  // 300KB 以上关闭高亮
  return PlainTextView(code: code);
}

7. RepoDetailPage 组合(把新功能接进去)

// pages/repo_detail_page.dart (核心结构示例)
class RepoDetailPage extends ConsumerWidget {
  final RepoId repoId;
  const RepoDetailPage({super.key, required this.repoId});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final branch = ref.watch(currentBranchProvider);

    return Scaffold(
      appBar: AppBar(
        title: Text('${repoId.owner}/${repoId.repo}'),
        actions: [
          Padding(
            padding: const EdgeInsets.symmetric(horizontal: 8),
            child: BranchSwitchBar(repoId: repoId),
          ),
        ],
      ),
      body: Column(
        children: [
          // 搜索
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: RepoSearchBar(repoId: repoId),
          ),

          // 上:目录树
          Expanded(
            flex: 3,
            child: RepoTreeFlatView(repoId: repoId),
          ),

          const Divider(height: 1),

          // 下:README
          Expanded(
            flex: 2,
            child: ReadmeView(repoId: repoId, branch: branch),
          ),
        ],
      ),
    );
  }
}

8. 小结

这一篇在前两篇基础上完成了:

  • 分支切换:ref 全局化,tree/readme/file 自动刷新;

  • 仓库搜索:基于全量树本地索引,零网络命中;

  • visibleNodes 扁平树:大仓库展开/收起不卡顿;

  • 三层缓存 + LRU:热数据秒开;

  • 并发防乱序:快速切目录不会闪回;

  • 高亮 isolate:大文件不卡 UI。
    所有点都来自前两篇的架构与优化方向,但这次是“能直接抄进项目跑起来”的落地版。 

本文介绍了Git代码仓库管理工具的深度优化与功能拓展方案。主要内容包括: 功能拓展: 实现分支/标签切换功能,通过全局状态管理自动刷新相关数据 新增仓库内文件搜索功能,基于本地索引实现快速检索 深度优化: 采用可视节点扁平化(visibleNodes)技术优化目录树渲染 实现三层LRU缓存机制提升数据加载效率 通过请求版本控制解决并发请求乱序问题 使用isolate隔离代码高亮处理,避免大文件卡顿 技术实现: 提供完整的Provider状态管理方案 展示缓存管理器的具体实现代码 详细说明各优化点的技术实现细

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐