在这里插入图片描述

项目背景与定位

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 兜底。路由参数统一通过 argumentsMap<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());
}

启动顺序至关重要:

  1. ensureInitialized — Flutter 引擎需要的时间不确定,必须先等待
  2. LocalStorage.init — AuthProvider 的 tryRestoreSession 需要读取已存储的 token
  3. OhosPlatform.init — 建立与 ArkTS 层的消息通道,登录功能依赖它
  4. runApp — 所有基础设施就绪后启动 UI,AuthProvider 在 create 中恢复 session
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐