# 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 app/src/main/java/net/opcapp/flash/ ├── app/ │ ├── LingjiApplication.kt │ ├── MainActivity.kt │ └── 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/ ├── home/ # 回访首页、最近一次、已上线次级入口 ├── question/ ├── casting/ ├── result/ ├── explanation/ ├── history/ └── settings/ ``` 测试按相同包结构镜像放入 `src/test` 和 `src/androidTest`。 当前已落地的包:`app/`、`core/model`、`core/designsystem`、`domain/casting`、`data/content`(接口与测试 fake)、`data/history`、`data/settings`、`feature/onboarding`、`feature/home`、`feature/question`、`feature/casting`、`feature/result`、`feature/settings`。尚未创建 `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` 接收第六爻锁定事件与当次 `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。 - **Room**:按保存策略自动写入或由用户当次明确保存的会话、解释和行动记录。Android 官方推荐 Room 管理非平凡结构化数据。[Room 文档](https://developer.android.com/training/data-storage/room) - **DataStore**:方法说明是否已读、主题、AI 同意版本和历史保存策略等少量设置;不保存大型历史或完整卦库。[DataStore 文档](https://developer.android.com/topic/libraries/architecture/datastore) - **SavedStateHandle**:恢复当前未完成流程;不是长期历史数据库。 历史 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` 使用实现时最新稳定且满足商店要求的版本,不在方案文档硬编码会迅速过期的数值。 - `minSdk=26` 已由 [ADR-014](decisions.md#adr-014正式-android-身份与最低版本) 确认;提高最低版本必须以依赖要求或覆盖率数据另立决策。 ## 11. 多模块化触发条件 满足以下至少两个条件后再评估从 `base` 迁移为多模块: - 两名以上开发者长期并行修改独立 feature; - 构建时间已经影响反馈循环; - 有可独立发布/复用的设计系统或领域库; - 历史、内容浏览、账户等功能使单模块边界难以机械约束; - 需要独立基准测试或测试应用。 迁移前必须新增 ADR;“项目看起来更专业”不是拆模块理由。