AtomGit Flutter鸿蒙客户端:仓库详情页

路由参数

仓库详情页通过命名路由 /repo 进入,接收 ownername 参数:

dart 复制代码
// 导航到详情页
Navigator.pushNamed(context, '/repo',
    arguments: {'owner': 'atomgit', 'name': 'flutter-ohos'});

// 详情页提取参数
final args =
    ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
final owner = args['owner'] as String;
final name = args['name'] as String;

RepoDetailProvider

Provider 在创建时即开始加载数据:

dart 复制代码
class RepoDetailProvider extends ChangeNotifier {
  final AtomGitApiClient _apiClient;
  Repository? _repository;
  String? _readme;
  bool _isLoading = false;
  String? _error;

  Future<void> load(String owner, String name) async {
    _isLoading = true;
    _error = null;
    notifyListeners();

    try {
      final encodedOwner = Uri.encodeComponent(owner);
      final encodedName = Uri.encodeComponent(name);

      final response = await _apiClient.get(
        '/repos/$encodedOwner/$encodedName',
      );

      final data = parseMap(response.data);
      if (data != null) {
        _repository = Repository.fromJson(data);
      }

      _readme = await _loadReadme(encodedOwner, encodedName,
          _repository?.defaultBranch ?? 'main');
    } on ApiException catch (e) {
      _error = e.message;
    } catch (e) {
      _error = '加载失败: $e';
    } finally {
      _isLoading = false;
      notifyListeners();
    }
  }
}

关键细节:owner 和 name 都经过 Uri.encodeComponent 编码,处理含特殊字符的仓库路径。

README 加载

README 加载采用静默失败策略 ------ 没有 README 不报错,只是不展示:

dart 复制代码
Future<String?> _loadReadme(
    String owner, String repo, String branch) async {
  try {
    final response = await _apiClient.get(
      '/repos/$owner/$repo/readme',
      queryParams: {'ref': branch},
    );
    final data = parseMap(response.data);
    if (data != null && data['content'] is String) {
      final content = data['content'] as String;
      return utf8.decode(base64.decode(content));
    }
  } on ApiException {
    // README 不存在是正常的,不报错
  } catch (_) {}
  return null;
}

API 返回的 README 内容是 Base64 编码的,解码后用 MarkdownViewer 渲染。

页面结构

RepoDetailScreen 是 StatelessWidget,通过 ChangeNotifierProvider 注入 Provider:

dart 复制代码
class RepoDetailScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final args =
        ModalRoute.of(context)!.settings.arguments as Map<String, dynamic>;
    final owner = args['owner'] as String;
    final name = args['name'] as String;

    return ChangeNotifierProvider(
      create: (_) => RepoDetailProvider(context.read<AtomGitApiClient>())
        ..load(owner, name),
      child: _RepoDetailBody(owner: owner, name: name),
    );
  }
}

自定义 Tab 栏

不使用 Flutter 的 TabBar/TabBarView,而是自定义 Row 按钮实现四个标签切换:

dart 复制代码
enum _DetailTab { code, issues, pulls, readme }

class _RepoDetailBodyState extends State<_RepoDetailBody> {
  _DetailTab _currentTab = _DetailTab.readme;

  Widget _buildTabBar() {
    return Material(
      elevation: 1,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
        child: Row(
          children: [
            _TabButton(
              label: '代码',
              isSelected: _currentTab == _DetailTab.code,
              onTap: () {
                Navigator.pushNamed(context, '/repo/code', arguments: {
                  'owner': widget.owner,
                  'name': widget.name,
                  'branch': provider.repository?.defaultBranch ?? 'main',
                });
              },
            ),
            _TabButton(
              label: 'Issues',
              isSelected: _currentTab == _DetailTab.issues,
              onTap: () {
                Navigator.pushNamed(context, '/repo/issues', arguments: {
                  'owner': widget.owner,
                  'name': widget.name,
                  'type': 'issue',
                });
              },
            ),
            _TabButton(
              label: 'PRs',
              isSelected: _currentTab == _DetailTab.pulls,
              onTap: () {
                Navigator.pushNamed(context, '/repo/pulls', arguments: {
                  'owner': widget.owner,
                  'name': widget.name,
                  'type': 'pr',
                });
              },
            ),
            _TabButton(
              label: 'README',
              isSelected: _currentTab == _DetailTab.readme,
              onTap: () => setState(() => _currentTab = _DetailTab.readme),
            ),
          ],
        ),
      ),
    );
  }
}

每个按钮的选中态通过底部蓝色指示条实现:

dart 复制代码
class _TabButton extends StatelessWidget {
  final String label;
  final bool isSelected;
  final VoidCallback onTap;

  Widget build(BuildContext context) {
    return Expanded(
      child: GestureDetector(
        onTap: onTap,
        child: Column(
          children: [
            Text(label, style: TextStyle(
              color: isSelected
                  ? Theme.of(context).colorScheme.primary
                  : null,
              fontWeight: isSelected ? FontWeight.w600 : null,
            )),
            if (isSelected)
              Container(
                height: 2,
                margin: const EdgeInsets.only(top: 4),
                color: Theme.of(context).colorScheme.primary,
              ),
          ],
        ),
      ),
    );
  }
}

头部信息区

仓库名、描述、统计数据以卡片形式展示:

dart 复制代码
Widget _buildRepoHeader(Repository repo) {
  return Card(
    margin: const EdgeInsets.all(16),
    child: Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          Row(children: [
            Icon(repo.isPrivate ? Icons.lock_outline : Icons.book_outlined,
                size: 20),
            const SizedBox(width: 8),
            Expanded(child: Text(repo.fullName,
                style: Theme.of(context).textTheme.titleMedium)),
          ]),
          if (repo.description != null) ...[
            const SizedBox(height: 8),
            Text(repo.description!),
          ],
          const SizedBox(height: 12),
          Row(children: [
            _StatItem(Icons.star_border, '${repo.stargazersCount}'),
            _StatItem(Icons.call_split, '${repo.forksCount}'),
            _StatItem(Icons.remove_red_eye_outlined, '${repo.watchersCount}'),
            _StatItem(Icons.error_outline, '${repo.openIssuesCount}'),
          ]),
        ],
      ),
    ),
  );
}

README 渲染

dart 复制代码
Widget _buildReadme(RepoDetailProvider provider) {
  if (provider.readme == null || provider.readme!.isEmpty) {
    return const Center(child: Text('暂无 README'));
  }
  return SingleChildScrollView(
    padding: const EdgeInsets.all(16),
    child: MarkdownViewer(markdown: provider.readme!),
  );
}

状态处理

三种状态的完整处理:

dart 复制代码
Widget _buildBody(RepoDetailProvider provider) {
  if (provider.error != null && provider.repository == null) {
    return ErrorRetryWidget(
      message: provider.error!,
      onRetry: () => provider.load(widget.owner, widget.name),
    );
  }
  if (provider.repository == null) {
    return const LoadingIndicator(message: '加载仓库信息...');
  }
  return Column(children: [
    _buildTabBar(),
    Expanded(child: _buildReadme(provider)),
  ]);
}

错误时展示可重试的错误页面,加载中展示 LoadingIndicator,成功后展示完整内容。

相关推荐
小雨青年1 小时前
鸿蒙 HarmonyOS 6 | Pura X Max 鸿蒙原生适配 19:设置页在 Pura X Max 上改成分组布局
华为·harmonyos
浮芷.1 小时前
鸿蒙PC端 TTS 并发调用问题详解:资源竞争与队列管理
算法·华为·开源·harmonyos·鸿蒙·鸿蒙系统
小雨下雨的雨1 小时前
基于鸿蒙PC Electron框架技术完成的表单验证技术详解
前端·javascript·华为·electron·前端框架·鸿蒙
提子拌饭1331 小时前
饮料含糖量查询应用 - 鸿蒙PC用Electron框架完整实现
前端·javascript·华为·electron·前端框架·鸿蒙
nashane2 小时前
HarmonyOS 6学习:句柄泄漏(Fd Leak)从“崩溃现场”到“代码行”的精准狙击指南
学习·华为·音视频·harmonyos
坚果派·白晓明2 小时前
[鸿蒙PC三方库移植适配] 使用 AtomCode + Skills 自动完成Protobuf鸿蒙化适配
c语言·c++·华为·harmonyos
世人万千丶2 小时前
鸿蒙PC异常解决:Install Failed: error: failed to install bundle.
服务器·华为·开源·harmonyos·鸿蒙
小雨下雨的雨2 小时前
iOS风格计算器 - 鸿蒙PC Electron框架上的技术实现详解
游戏·ios·华为·electron·harmonyos·鸿蒙
小雨下雨的雨3 小时前
五子棋AI在鸿蒙PC Electron上的实现的原理与实践
人工智能·游戏·华为·electron·harmonyos·鸿蒙