项目背景与定位
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/不会直接 importissue/的代码——如果需要从仓库详情跳转到 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字段,构造后不可变:
classRepository{finalint id;finalStringname;finalStringfullName;// ...所有字段均为 finalconstRepository({requiredthis.id,requiredthis.name,// ...});}不可变模型在 Widget 树中的优势:Flutter 通过identical比较判断 Widget 是否需要重建。不可变对象在数据未变化时可以安全复用旧引用,避免不必要的重建。
3. Provider 依赖注入
全局依赖在应用根部注入:
MultiProvider (在 AtomGitApp 中) ├── Provider<AtomGitApiClient> ← 全局服务,不变 │ 所有页面的 Provider 通过 context.read<> 获取 │ └── ChangeNotifierProvider<AuthProvider> ← 全局状态,变化时通知 所有 build 中通过 context.watch<> 订阅页面级 Provider 在各页面的 build 方法中创建,生命周期与页面绑定(页面 pop 时自动 dispose):
classRepoDetailScreenextendsStatelessWidget{@overrideWidgetbuild(BuildContextcontext){returnChangeNotifierProvider(create:(_)=>RepoDetailProvider(context.read<AtomGitApiClient>())..load(owner,name),child:_RepoDetailBody(/* ... */),);}}页面级 Provider 只在当前页面及其子组件中可用,离开页面即销毁,不会造成内存泄漏。
4. 安全 JSON 解析层
所有 API 响应经过统一的类型安全处理链:
HTTP Response → jsonDecode → dynamic → _unwrapEnvelope → parseList/parseMap → whereType<T> → Model.fromJsonjson_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:
staticRoute<dynamic>generateRoute(RouteSettingssettings){switch(settings.name){case'/':returnMaterialPageRoute(builder:(_)=>constMainShell());case'/login':returnMaterialPageRoute(builder:(_)=>constLoginScreen());case'/search':returnMaterialPageRoute(builder:(_)=>constSearchScreen());case'/repo':returnMaterialPageRoute(builder:(_)=>constRepoDetailScreen());case'/repo/code':returnMaterialPageRoute(builder:(_)=>constFileTreeScreen());case'/repo/blob':returnMaterialPageRoute(builder:(_)=>constCodeViewScreen());case'/repo/issues':returnMaterialPageRoute(builder:(_)=>constIssueListScreen());case'/repo/issues/detail':returnMaterialPageRoute(builder:(_)=>constIssueDetailScreen());case'/repo/pulls':returnMaterialPageRoute(builder:(_)=>constIssueListScreen());case'/repo/pulls/detail':returnMaterialPageRoute(builder:(_)=>constIssueDetailScreen());case'/user':returnMaterialPageRoute(builder:(_)=>constProfileScreen());case'/starred':returnMaterialPageRoute(builder:(_)=>constStarredReposScreen());case'/settings':returnMaterialPageRoute(builder:(_)=>constSettingsScreen());default:returnMaterialPageRoute(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.dartvoidmain()async{// 步骤 1:确保 Flutter 引擎就绪WidgetsFlutterBinding.ensureInitialized();// 步骤 2:初始化本地存储(必须早于 AuthProvider)finalappDocPath=awaitgetApplicationDocumentsDirectory();awaitLocalStorage.instance.init(appDocPath);// 步骤 3:初始化 HarmonyOS 平台通道OhosPlatform.instance.init();// 步骤 4:启动 Widget 树runApp(constAtomGitApp());}启动顺序至关重要:
ensureInitialized— Flutter 引擎需要的时间不确定,必须先等待LocalStorage.init— AuthProvider 的tryRestoreSession需要读取已存储的 tokenOhosPlatform.init— 建立与 ArkTS 层的消息通道,登录功能依赖它runApp— 所有基础设施就绪后启动 UI,AuthProvider 在create中恢复 session