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,成功后展示完整内容。

相关推荐
stringwu9 小时前
Flutter 开发必备:MVI 架构的高效实现指南
前端·flutter
程序员老刘1 天前
Flutter版本选择指南:3.44系列继续观望 | 2026年6月
flutter·ai编程·客户端
花椒技术2 天前
HJPusher / HJPlayer SDK 实践:我们为什么把直播推播链路拆成一套可复用能力
设计模式·harmonyos·直播
用户965597361903 天前
Provider vs Bloc vs GetX vs Riverpod:Flutter 状态管理方案怎么选?
flutter
一维Ace3 天前
HarmonyOS ArkTS 按钮组件全解:Button、Toggle 状态交互实战
harmonyos
恋猫de小郭3 天前
Flutter Patchwork,不用 Fork 改依赖包源码的第三方工具
android·前端·flutter
程序员老刘3 天前
跑分第一的编程大模型,我为啥不用?
flutter·ai编程·vibecoding
anyup4 天前
来简单聊聊鸿蒙开发,万元奖金的事~
前端·华为·harmonyos
Georgewu4 天前
【无测试机别害怕】华为云鸿蒙云手机南:从零到联调全流程详解
harmonyos
恋猫de小郭4 天前
苹果 AirPods 协议,Android 也可以使用完整版 AirPods 能力
android·前端·flutter