AtomGit Flutter鸿蒙客户端:项目架构概览
AtomGit 口袋工具是一个基于 Flutter 开发的 OpenHarmony 客户端应用,对接 AtomGit v5 REST API。AtomGit 是由开放原子开源基金会运营的代码托管平台,为中国开发者提供类似 GitHub 的 Git 仓库管理、Issue 跟踪和 Pull Request 协作功能。

项目背景与定位
AtomGit 口袋工具是一个基于 Flutter 开发的 OpenHarmony 客户端应用,对接 AtomGit v5 REST API。AtomGit 是由开放原子开源基金会运营的代码托管平台,为中国开发者提供类似 GitHub 的 Git 仓库管理、Issue 跟踪和 Pull Request 协作功能。
HarmonyOS 作为华为自主研发的分布式操作系统,其应用生态对高质量开发工具有迫切需求。本项目选择 Flutter 作为跨平台框架出于三个核心考量:Flutter 官方对 HarmonyOS 的持续适配支持、高性能的自渲染引擎带来的原生级体验、以及一套 Dart 代码跨平台复用的开发效率优势。
核心技术栈
| 层级 | 技术选择 | 版本 | 选择原因 |
|---|---|---|---|
| UI 框架 | Flutter | 3.22+ | 跨平台渲染引擎,HarmonyOS 官方支持 |
| 目标平台 | HarmonyOS (OpenHarmony) | API 12+ | 鸿蒙原生应用,通过 flutter_ohos 运行 |
| 编程语言 | Dart | 3.5+ | Flutter 原生语言,支持 AOT 编译 |
| 原生交互 | ArkTS | API 12 | HarmonyOS 原生能力调用(浏览器、URL Scheme) |
| 状态管理 | Provider | 6.1.0 | 轻量、成熟、学习曲线平缓 |
| HTTP 通信 | dart:http | 1.2.0 | Flutter 官方推荐的 HTTP 客户端 |
| Markdown 渲染 | flutter_markdown | 0.7.0 | 原生 Widget 渲染,无需 WebView |
| 日期国际化 | intl | 0.19.0 | Dart 官方国际化库,支持中文格式化 |
| 代码规范 | flutter_lints | 5.0.0 | Flutter 官方推荐 lint 规则集 |
项目目录结构

ohos/
├── lib/ # Dart 业务代码
│ ├── main.dart # 应用入口:初始化存储、平台通道
│ ├── app.dart # MaterialApp 配置、路由表、Provider 注入
│ │
│ ├── core/ # 核心基础设施层(零业务依赖)
│ │ ├── constants/
│ │ │ └── api_constants.dart # API 端点、版本号、限流参数、OAuth 参数
│ │ ├── network/
│ │ │ └── api_client.dart # HTTP 封装:Header、信封解包、错误映射、限流追踪
│ │ ├── storage/
│ │ │ └── local_storage.dart # 文件级 JSON 键值持久化
│ │ ├── theme/
│ │ │ └── app_theme.dart # Material 3 主题:ColorScheme.fromSeed
│ │ ├── platform/
│ │ │ └── ohos_platform.dart # HarmonyOS 平台通道 Dart 端
│ │ └── utils/
│ │ ├── json_parser.dart # 安全 JSON 解析:parseInt/parseString/parseList/parseMap
│ │ └── date_formatter.dart # 日期工具:相对时间(中文)+ 完整格式
│ │
│ ├── features/ # 功能模块(按业务领域组织)
│ │ ├── shell/
│ │ │ └── main_shell.dart # 底部导航主框架
│ │ ├── auth/ # 认证模块
│ │ │ ├── providers/auth_provider.dart # 全局登录状态管理
│ │ │ ├── services/auth_service.dart # OAuth URL 构建、Token 交换
│ │ │ └── screens/login_screen.dart # 三重登录入口 UI
│ │ ├── home/
│ │ │ └── home_tab.dart # 首页 Tab:热门仓库 + 用户仓库
│ │ ├── explore/
│ │ │ └── explore_tab.dart # 发现 Tab:搜索 + 推荐
│ │ ├── notifications/
│ │ │ └── notifications_tab.dart # 通知 Tab:占位 + 登录引导
│ │ ├── profile/
│ │ │ └── profile_tab.dart # 我的 Tab:手动 Provider 生命周期
│ │ ├── repo/ # 仓库模块(核心)
│ │ │ ├── models/repository.dart # 仓库数据模型
│ │ │ ├── widgets/repo_card.dart # 仓库卡片共享组件
│ │ │ ├── providers/
│ │ │ │ ├── repo_detail_provider.dart # 仓库详情 + README
│ │ │ │ ├── repo_search_provider.dart # 搜索 + 分页
│ │ │ │ └── starred_repos_provider.dart # 收藏列表 + 分页回退
│ │ │ └── screens/
│ │ │ ├── repo_detail_screen.dart # 详情页:自定义 Tab 栏
│ │ │ ├── search_screen.dart # 搜索页:全屏搜索
│ │ │ └── starred_repos_screen.dart # 收藏页:无限滚动
│ │ ├── code/ # 代码浏览模块
│ │ │ ├── models/file_node.dart # 文件节点(树/文件)
│ │ │ ├── providers/code_provider.dart # 文件树 + 文件内容
│ │ │ └── screens/
│ │ │ ├── file_tree_screen.dart # 文件树页:原地目录导航
│ │ │ └── code_view_screen.dart # 代码页:带行号渲染
│ │ ├── issue/ # Issue 模块
│ │ │ ├── models/issue.dart # Issue + Comment 模型
│ │ │ ├── providers/issue_provider.dart # Issue/PR 列表 + 详情 + 状态过滤
│ │ │ └── screens/
│ │ │ ├── issue_list_screen.dart # Issue 列表:FilterChip 状态切换
│ │ │ └── issue_detail_screen.dart # Issue 详情:评论列表 + Markdown
│ │ ├── user/ # 用户模块
│ │ │ ├── models/user_profile.dart # 用户资料模型
│ │ │ ├── providers/user_provider.dart # 双模式:自己/他人
│ │ │ └── screens/profile_screen.dart # 用户资料页:统计 + 仓库列表
│ │ └── settings/
│ │ └── screens/settings_screen.dart # 设置页:账户 + API + 关于
│ │
│ └── shared/ # 跨功能共享 UI 组件
│ └── widgets/
│ ├── error_retry_widget.dart # 错误状态 + 重试按钮
│ ├── loading_indicator.dart # 加载中指示器
│ ├── markdown_viewer.dart # Markdown 主题渲染
│ ├── paginated_list.dart # 通用分页列表
│ └── user_avatar.dart # 用户头像(网络 + 首字母 Fallback)
│
├── ohos/ # HarmonyOS 原生层
│ └── entry/src/main/
│ ├── module.json5 # 应用能力声明:URI Scheme、网络权限
│ └── ets/
│ ├── entryability/
│ │ └── EntryAbility.ets # Flutter 引擎宿主、平台通道 ArkTS 端
│ ├── pages/
│ │ └── Index.ets # ArkUI 入口页,承载 FlutterPage
│ └── plugins/
│ └── GeneratedPluginRegistrant.ets # 插件注册(当前无第三方插件)
│
├── pubspec.yaml # Dart 依赖 + Flutter 配置
└── analysis_options.yaml # Lint 规则(flutter_lints)
目录设计的核心原则
core/ 零业务依赖。core/ 目录下的所有代码不引用 features/ 中的任何模块。网络层只关心 HTTP 协议细节,不关心是仓库还是 Issue 在调用它。存储层只关心文件的读写,不关心存储的是 Token 还是用户偏好。这种单向依赖使得 core 具有高稳定性和可测试性——修改仓库详情页的逻辑不会意外破坏 JSON 解析器的行为。
features/ 自包含。每个功能模块在自身目录内自给自足——models/ 定义数据结构,providers/ 管理状态和 API 调用,screens/ 实现界面。模块之间不直接相互引用。例如 repo/ 不会直接 import issue/ 的代码——如果需要从仓库详情跳转到 Issue 列表,通过 Navigator 路由实现;如果需要共享数据,通过全局 Provider 传递。
shared/ 纯 UI 组件。共享组件只有渲染逻辑,不包含业务数据访问。它们接收参数并返回 Widget 树,可以被任何 feature 安全引用。
架构原则
1. 按功能领域划分(Feature-based Architecture)
项目的目录组织采用功能领域划分而非技术类型划分:
// 不按技术类型分(不好)
lib/
models/ ← 所有模型混在一起
screens/ ← 所有页面混在一起
providers/ ← 所有 Provider 混在一起
// 按功能领域分(本项目采用)
lib/features/
repo/ ← 仓库相关的 model + screen + provider
issue/ ← Issue 相关的 model + screen + provider
user/ ← 用户相关的全部代码
功能领域划分使得修改单一功能时可以聚焦在一个目录中,不涉及跨目录的文件跳转。新增功能只需在 features/ 下创建一个新目录,不影响已有代码。
2. 不可变数据模型
所有 Model 类使用 final 字段,构造后不可变:
class Repository {
final int id;
final String name;
final String fullName;
// ...所有字段均为 final
const Repository({
required this.id,
required this.name,
// ...
});
}
不可变模型在 Widget 树中的优势:Flutter 通过 identical 比较判断 Widget 是否需要重建。不可变对象在数据未变化时可以安全复用旧引用,避免不必要的重建。
3. Provider 依赖注入
全局依赖在应用根部注入:
MultiProvider (在 AtomGitApp 中)
├── Provider<AtomGitApiClient> ← 全局服务,不变
│ 所有页面的 Provider 通过 context.read<> 获取
│
└── ChangeNotifierProvider<AuthProvider> ← 全局状态,变化时通知
所有 build 中通过 context.watch<> 订阅
页面级 Provider 在各页面的 build 方法中创建,生命周期与页面绑定(页面 pop 时自动 dispose):
class RepoDetailScreen extends StatelessWidget {
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (_) =>
RepoDetailProvider(context.read<AtomGitApiClient>())
..load(owner, name),
child: _RepoDetailBody(/* ... */),
);
}
}
页面级 Provider 只在当前页面及其子组件中可用,离开页面即销毁,不会造成内存泄漏。
4. 安全 JSON 解析层
所有 API 响应经过统一的类型安全处理链:
HTTP Response → jsonDecode → dynamic → _unwrapEnvelope → parseList/parseMap → whereType<T> → Model.fromJson
json_parser.dart 中的解析函数永不抛出异常,所有类型不匹配都有兜底默认值。这解决了 AtomGit v5 API 中 int 字段可能返回 String 的类型不一致问题。每一层防护的失效都不会导致应用崩溃。
5. 分层错误处理
错误从网络层逐步向上传递,每一层有自己的处理职责:
API HTTP 错误
→ AtomGitApiClient._mapError(statusCode)
→ ApiException (带用户可读中文消息)
→ Provider catch ApiException
→ _error = e.message, notifyListeners()
→ UI Widget 检测 _error
→ ErrorRetryWidget (展示错误 + 提供重试按钮)
Provider 层是错误处理的边界。Provider 的 try-catch 确保异常不会穿透到 UI 层。UI 层只需要检查 provider.error 是否为 null 来决定展示什么状态,不需要写 try-catch。
路由设计
项目采用命名路由 + 集中式 onGenerateRoute:
static Route<dynamic> generateRoute(RouteSettings settings) {
switch (settings.name) {
case '/':
return MaterialPageRoute(builder: (_) => const MainShell());
case '/login':
return MaterialPageRoute(builder: (_) => const LoginScreen());
case '/search':
return MaterialPageRoute(builder: (_) => const SearchScreen());
case '/repo':
return MaterialPageRoute(builder: (_) => const RepoDetailScreen());
case '/repo/code':
return MaterialPageRoute(builder: (_) => const FileTreeScreen());
case '/repo/blob':
return MaterialPageRoute(builder: (_) => const CodeViewScreen());
case '/repo/issues':
return MaterialPageRoute(builder: (_) => const IssueListScreen());
case '/repo/issues/detail':
return MaterialPageRoute(builder: (_) => const IssueDetailScreen());
case '/repo/pulls':
return MaterialPageRoute(builder: (_) => const IssueListScreen());
case '/repo/pulls/detail':
return MaterialPageRoute(builder: (_) => const IssueDetailScreen());
case '/user':
return MaterialPageRoute(builder: (_) => const ProfileScreen());
case '/starred':
return MaterialPageRoute(builder: (_) => const StarredReposScreen());
case '/settings':
return MaterialPageRoute(builder: (_) => const SettingsScreen());
default:
return MaterialPageRoute(builder: (_) => const _NotFoundScreen());
}
}
包含 12 个有效路由和 1 个 404 兜底。路由参数统一通过 arguments 以 Map<String, dynamic> 传递。
导航层级
路由设计遵循 Tab 内切换 vs 全屏覆盖的分层规则:
root Navigator
├── MainShell (IndexedStack) ← '/' 路由
│ ├── HomeTab ← Tab 内部切换(IndexedStack)
│ ├── ExploreTab
│ ├── NotificationsTab
│ └── ProfileTab
├── SearchScreen ← '/search'(全屏覆盖 Tab)
├── RepoDetailScreen ← '/repo'(全屏覆盖 Tab)
├── FileTreeScreen ← '/repo/code'
└── ...(所有详情页全屏覆盖)
核心规则:Tab 切换在 IndexedStack 内部完成,不产生 Navigator 栈变化。所有详情页通过 Navigator.pushNamed 推入 root Navigator,全屏显示(覆盖底部 Tab 栏)。返回时 Tab 的完整状态(滚动位置、已加载数据)统一恢复。
HarmonyOS 平台集成架构
Flutter 与 HarmonyOS 原生层通过 BasicMessageChannel 双向通信:
Flutter (Dart) ←→ "com.atomgit/auth" ←→ HarmonyOS (ArkTS)
OhosPlatform EntryAbility
- openBrowser(url) - handleMessage()
- onAuthCode Stream - onNewWant()
通信协议:
- 信道名:
com.atomgit/auth - 编解码器:
StringCodec(UTF-8 字符串) - 消息格式:JSON 文本,
method字段区分请求类型 - 请求方向:Dart 发送
{method: "openBrowser", url: "..."},ArkTS 执行后返回{success: true} - 回调方向:ArkTS 在 OAuth 回调时发送
{type: "authCode", code: "..."},Dart 通过 Broadcast Stream 分发
数据流全貌
一个典型的从 API 到 UI 的完整数据流:
1. 用户进入仓库详情页
Navigator.pushNamed('/repo', args: {owner, name})
2. RepoDetailScreen.build()
ChangeNotifierProvider(create: RepoDetailProvider..load())
3. RepoDetailProvider.load()
apiClient.get('/repos/$owner/$name')
→ _buildUri() 添加 query 参数
→ _headers 添加 Authorization: Bearer <token>
→ http.get(uri, headers)
→ 收到 HTTP Response
→ _updateRateLimit() 更新限流信息
→ _processResponse()
→ statusCode 200 → _unwrapEnvelope()
→ statusCode 40x/50x → _mapError() → 抛 ApiException
4. Provider 解析响应
parseMap(response.data)
→ 从 {data: {code:200, message:"ok", data:{...}}} 解包
→ 提取仓库 JSON 对象
Repository.fromJson(map)
→ parseInt(json['id']) — 安全解析
→ parseString(json['full_name']) — 安全解析
→ parseDateTime(json['created_at']) — 安全解析
notifyListeners()
5. UI 重建
context.watch<RepoDetailProvider>()
provider.repository != null
→ 渲染仓库头部 + README
关键技术决策
| 决策点 | 本项目的选择 | 权衡考量 |
|---|---|---|
| 状态管理 | Provider | 轻量级。足够应对当前应用复杂度。不引入 Riverpod/Bloc 的额外概念负担 |
| JSON 解析 | 手写安全解析 | API 类型不稳定,代码生成(json_serializable)产生的 as 强转会崩溃 |
| 认证方式 | Token 直接输入 | 优先保证可用性。OAuth 作为高级路径同时支持 |
| 本地存储 | 文件 JSON | 数据量很小(仅 token)。不需要 SQLite 的查询能力和 SharedPreferences 的平台依赖 |
| 底部导航 | IndexedStack | 4 个 Tab 全部保持状态,切换零延迟。内存开销可接受 |
| Markdown 渲染 | flutter_markdown | 原生 Widget 渲染,性能好,主题可深度定制。不需要 WebView 的内存开销 |
| 主题系统 | ColorScheme.fromSeed | 一个种子色自动生成完整调色板 + 深色模式。不需要手动定义 30+ 颜色 |
| 路由管理 | onGenerateRoute | 集中管理,支持守卫、404 兜底。没有使用 go_router 因为路由结构简单 |
| 平台通信 | BasicMessageChannel | 直接使用 Flutter 平台通道 API,不引入额外桥接框架 |
应用启动流程
// main.dart
void main() async {
// 步骤 1:确保 Flutter 引擎就绪
WidgetsFlutterBinding.ensureInitialized();
// 步骤 2:初始化本地存储(必须早于 AuthProvider)
final appDocPath = await getApplicationDocumentsDirectory();
await LocalStorage.instance.init(appDocPath);
// 步骤 3:初始化 HarmonyOS 平台通道
OhosPlatform.instance.init();
// 步骤 4:启动 Widget 树
runApp(const AtomGitApp());
}
启动顺序至关重要:
ensureInitialized— Flutter 引擎需要的时间不确定,必须先等待LocalStorage.init— AuthProvider 的tryRestoreSession需要读取已存储的 tokenOhosPlatform.init— 建立与 ArkTS 层的消息通道,登录功能依赖它runApp— 所有基础设施就绪后启动 UI,AuthProvider 在create中恢复 session
更多推荐

所有评论(0)