Files
brainwave/docs/architecture.md
T

198 lines
11 KiB
Markdown
Raw Normal View History

# Android 系统架构
> 状态:已接受的 MVP 架构
> 技术方向:Kotlin + Jetpack Compose,单模块起步
> 骨架来源:[Android Architecture Starter Template — base](https://github.com/android/architecture-templates/tree/base)
## 1. 架构目标
- 本地起卦是确定、可测试、无 Android/网络依赖的纯领域逻辑。
- UI、内容、存储和 AI 可以独立替换,不改变已生成的 `CastResult`。
- 核心流程离线可用,AI 故障只降级解释能力。
- 包边界对人和编码代理都清楚,并能通过测试或静态规则检查。
- MVP 保持单 Gradle 模块,避免过早多模块化;代码仍按领域和层分包。
Android 官方建议新应用采用清晰的 UI/数据分层、单向数据流、ViewModel、协程与 Flow;大型复杂业务才按需要增加 domain 层。本项目因为起卦不变量重要,保留轻量 domain 层。[Android 架构指南](https://developer.android.com/topic/architecture)
## 2. 系统上下文
```text
┌──────────────── Android App ────────────────┐
│ Compose UI │
│ ↓ intent ↑ immutable state │
│ ViewModel / state holder │
│ ↓ │
│ Domain: CastEngine / explanation use cases │
│ ↓ interfaces │
│ Repositories │
│ ├─ Assets / prepackaged content │
│ ├─ Room history │
│ ├─ DataStore settings │
│ └─ AI gateway ───────────────→ App backend│
└─────────────────────────────────────────────┘
```
只有 AI 解释需要网络。起念、投币、起卦、卦库读取和本地解释都位于 Android 应用内部。
## 3. 建议目录
工程初始化后建议采用:
```text
2026-08-07 18:00:38 +08:00
app/src/main/java/net/opcapp/flash/
├── app/
2026-08-07 18:00:38 +08:00
│ ├── LingjiApplication.kt
│ ├── MainActivity.kt
2026-08-07 18:00:38 +08:00
│ └── LingjiApp.kt
├── core/
│ ├── model/ # 跨层不可变领域模型
│ ├── designsystem/ # 主题、字体、间距、卦象 Canvas
│ └── common/ # 极少量通用结果类型/调度器
├── domain/
│ ├── casting/ # CastEngine、映射与校验
│ └── explanation/ # 解释用例与端口
├── data/
│ ├── content/ # assets 卦库解析与版本校验
│ ├── history/ # Room entity、DAO、repository
│ ├── settings/ # DataStore
│ └── ai/ # 网络 DTO、gateway、响应校验
└── feature/
├── onboarding/
2026-08-04 23:00:05 +08:00
├── home/ # 回访首页、最近一次、已上线次级入口
├── question/
├── casting/
├── result/
├── explanation/
├── history/
└── settings/
```
测试按相同包结构镜像放入 `src/test` 和 `src/androidTest`。
当前已落地的包:`app/`、`core/model`、`core/designsystem`、`domain/casting`、`data/content`(assets 解析器与测试 fake)、`data/history`、`data/settings`、`feature/onboarding`、`feature/home`、`feature/question`、`feature/casting`、`feature/result`、`feature/settings`、`feature/content`。尚未创建 `feature/history`、`feature/explanation`、`domain/explanation` 和 `data/ai`。首页不为问卦簿或「解」生成空占位。
## 4. 依赖方向
允许:
```text
feature UI → feature ViewModel → domain use case → repository interface
data implementation → repository interface + core model
app/navigation → feature public route
core/designsystem → Compose/Material + core model(仅绘制需要)
```
禁止:
- `domain` 依赖 Compose、Activity、ViewModel、Room、Retrofit/Ktor 或具体 AI SDK。
- `CastEngine` 读取系统时间、随机数、网络、数据库或问题文本。
- Composable 直接访问 DAO、assets、网络客户端或 Hilt entry point。
- `data/ai` 引用或调用 `CastEngine`。
- AI DTO 直接成为 UI 状态;必须先校验并映射为领域结果。
- feature 之间直接引用对方的内部 ViewModel 或 screen 实现。
- 一个“Utils”包承载无边界的杂项业务逻辑。
工程具备代码后,应通过架构测试或静态检查机械执行这些禁止项,不能只依赖评审记忆。
## 5. 核心组件职责
### `CastEngine`
纯 Kotlin、无副作用。输入六轮铜币,使用 API 固定的 `coin-v1` 约定输出不可变 `CastComputation`;随后由记录工厂附加 `contentVersion` 与 `createdAt`,组成不可变 `CastResult`。方法版本不是调用方可随意传入的自由字符串,记录元数据也不能进入计算。所有规则来自[领域规则](domain-rules.md)。
### `HexagramContentRepository`
按卦号和 `contentVersion` 返回已校验的本地内容。缺失、重复或版本不兼容属于数据完整性错误,不使用 AI 猜补。
### `CastingSessionViewModel`
维护草稿问题、六轮录入和状态机。通过 `SavedStateHandle` 保存可恢复的进行中状态;只在第六轮确认时调用 `CastEngine`。
### `HistoryRepository`
2026-08-04 23:00:05 +08:00
接收第六爻锁定事件与当次 `HistorySavePolicy` 快照,在 `autoSaveHistory=true` 时持久化会话;总开关关闭时仍支持结果页明确发出的“保存本次”命令。策略包含 `saveQuestion`、`saveExplanation` 和 `saveActionNote`,生产默认值均为 `true`,由 DataStore 提供但不得泄露到领域起卦逻辑。
Repository 提供倒序历史与最新一条的 `Flow` 查询,并把随后产生的本地或 AI 解读事务性关联到同一 session。重新解读新增版本或执行经过明确确认的替换命令,禁止普通 upsert 静默覆盖。DAO 事务负责 session、explanation 和 action 的一致写入与级联删除;首页直接订阅最新记录的投影,不维护第二份敏感正文缓存。Room 模型不得泄露到 UI;数据库迁移必须有测试。
### `HomeViewModel` / `HistoryViewModel`
`HomeViewModel` 组合 onboarding 状态、最新历史投影、保存策略摘要和已启用功能入口,只输出真实可进入的入口;不为未来功能生成禁用占位。首页固定输出本机保存/AI 发送边界文案和设置入口。`HistoryViewModel` 维护列表、空态、详情、单条删除和清空全部确认状态,通过 `HistoryRepository` 完成操作。导航使用一次性 UI 事件或 NavHost 回调,返回后恢复列表位置。
### `ExplanationRepository`
提供统一接口:本地解释和 AI 解释返回相同的可展示领域结构,并带明确 `source`。AI 实现遵循[AI 解释与安全](ai-safety.md)。
## 6. 单向数据流
每个 feature 使用不可变 `UiState` 和显式 `UiAction`:
```text
user action → ViewModel → use case/repository → state update → Compose render
```
一次性结果也应建模为状态及消费动作,避免裸 `Channel`/事件在配置变化时丢失。导航由 UI 根据已处理状态触发,并保证重复收集不会重复提交 AI 请求。
领域计算成功后 `CastResult` 为只读值。解释状态与起卦状态分开存储:
```text
ResultUiState(
castResult = immutable,
content = immutable,
explanationState = Idle | Loading | Local | Ai | Failed
)
```
## 7. 数据和存储选择
- **assets 或预置 Room**:版本化的 64 卦及爻辞。首版如果仅按编号读取,JSON assets 更简单;需要全文检索或内容迁移时再采用预置 Room。
2026-08-04 23:00:05 +08:00
- **Room**:按保存策略自动写入或由用户当次明确保存的会话、解释和行动记录。Android 官方推荐 Room 管理非平凡结构化数据。[Room 文档](https://developer.android.com/training/data-storage/room)
- **DataStore**:方法说明是否已读、主题、AI 同意版本和历史保存策略等少量设置;不保存大型历史或完整卦库。[DataStore 文档](https://developer.android.com/topic/libraries/architecture/datastore)
- **SavedStateHandle**:恢复当前未完成流程;不是长期历史数据库。
2026-08-04 23:00:05 +08:00
历史 Room 数据库及其辅助文件必须通过 Android backup/data-extraction 配置排除系统自动备份和设备迁移。此边界由构建配置测试机械验证,不能只依赖界面文案;引入导出、账号或同步前必须新增 ADR。
详细契约见[数据与内容](data-content.md)。
## 8. 网络与 AI 边界
正式发行版本通过自有后端代理模型服务:
```text
App → HTTPS backend → model provider
```
开发者密钥不能放入 APK。网络层只在用户主动选择 AI 解读后工作,设置合理连接/读取超时、取消传播和有限重试。不得后台预取解释,不得在用户继续编辑问题时偷偷重发。
如果未来支持用户自带密钥,它是单独的产品模式,需要 Android Keystore、清晰风险说明和独立决策记录,不能与默认发布路径混合。
## 9. 安全与隐私
- 仅请求联网所需权限;起卦不需要相机、定位、联系人、传感器或存储权限。
- 所有请求使用 TLS,服务端执行认证、限流和滥用保护。
- 日志不得包含问题原文、完整提示词、模型回复或 API 密钥。
- 崩溃与分析事件只记录枚举状态、耗时桶和匿名错误码。
- 用户删除本地记录后,不保留隐藏副本;服务端数据保留策略需在 AI 上线前单独确认。
- 任何来自 assets、数据库或网络的数据在边界处解析和校验,失败后进入有恢复路径的错误状态。
## 10. 依赖和构建政策
- 版本集中在 Gradle Version Catalog。
- 优先使用 AndroidX、Kotlin 官方组件和维护活跃的小型依赖。
- 新依赖必须说明用途、维护状态、许可证、体积和是否可由现有能力替代。
- 禁止直接引入完整“算命 SDK”或无法审计的卦象计算库。
- Compose 依赖使用 BOM 对齐版本。
- `compileSdk`/`targetSdk` 使用实现时最新稳定且满足商店要求的版本,不在方案文档硬编码会迅速过期的数值。
2026-08-07 18:00:38 +08:00
- `minSdk=26` 已由 [ADR-014](decisions.md#adr-014正式-android-身份与最低版本) 确认;提高最低版本必须以依赖要求或覆盖率数据另立决策。
## 11. 多模块化触发条件
满足以下至少两个条件后再评估从 `base` 迁移为多模块:
- 两名以上开发者长期并行修改独立 feature;
- 构建时间已经影响反馈循环;
- 有可独立发布/复用的设计系统或领域库;
- 历史、内容浏览、账户等功能使单模块边界难以机械约束;
- 需要独立基准测试或测试应用。
迁移前必须新增 ADR;“项目看起来更专业”不是拆模块理由。