diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a8a3857 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,67 @@ +# 明缘进阶场 Android 客户端 + +「明缘进阶场」的 Android 客户端(`com.mingyuan.android_client`),对接 `wechat-backend` 提供的 auth / checkin / plan / course-vote / survey / feedback / profile API。当前处于**基础建设阶段**:多模块骨架已就绪,各 feature 为空占位,尚未接入真实后端。 + +## 快速开始 + +```bash +./gradlew :app:assembleDebug # 构建调试 APK +./gradlew test testDebugUnitTest # 运行单元测试(JVM + Android) +bash scripts/verify.sh # 完整验证(格式 / 测试 / 构建 / 架构检查) +``` + +## 架构概览 + +多模块 NIA 架构:`app` 是组合根(Hilt 入口 + 单一 `AppNavHost`),`feature/*` 承载各业务界面,`core/*` 提供共享能力。依赖方向严格单向,由 `verifyArchitecture` Gradle 任务强制执行(违规即失败)。详见 [docs/architecture.md](docs/architecture.md)。 + +## 目录结构 + +``` +app/ 组合根:MingyuanApplication、MainActivity、AppNavHost +core/common 纯 Kotlin 工具(无 Android 依赖) +core/model 领域模型(纯 Kotlin) +core/domain UseCase + Repository 接口(纯 Kotlin) +core/data Repository 实现,编排 network/database/datastore +core/network Retrofit/OkHttp、DTO、序列化 +core/database Room 实体 / DAO / 数据库 +core/datastore DataStore 偏好 +core/designsystem 主题 / 颜色 / 字体 / 通用组件 +core/ui 共享 Compose UI + 导航辅助 +feature/* 业务功能(auth/checkin/plan/course/survey/feedback/profile) +build-logic/ 约定插件(mcl.*)+ verifyArchitecture +docs/ 架构 / 规范 / 决策记录(见下方文档地图) +scripts/verify.sh 确定性验证脚本 +``` + +## 关键约定 + +1. **依赖方向单向**:`app → 所有`;`feature/* → core(ui/designsystem/domain/model)`;`core:data → domain/model/network/database/datastore`;`core:model`/`core:domain` 保持纯 Kotlin、不依赖 Android。新增模块或跨层依赖必须同步更新 `build-logic` 的 `RootPlugin.kt` allow-list 与 `docs/architecture.md`。 +2. **版本唯一事实源**:所有依赖版本在 [gradle/libs.versions.toml](gradle/libs.versions.toml),禁止在模块里硬编码版本号。 +3. **模块配置走约定插件**:每个模块只声明 `mcl.*` 插件 + namespace,不要手写 `android {}` 模板代码。新增模块/插件流程见 [build-logic/convention/DESIGN_NOTES.md](build-logic/convention/DESIGN_NOTES.md)。 +4. **确定性格式化**:`spotlessCheck` 必须通过(ktlint 规则在 [.editorconfig](.editorconfig));提交前用 `./gradlew spotlessApply` 修正。 +5. **命名空间规则**:`com.mingyuan.android_client.<路径点分>`(如 `feature:checkin` → `com.mingyuan.android_client.feature.checkin`)。 +6. **验证优先于声称完成**:任何"完成/通过"声明都必须以 `scripts/verify.sh` 的新鲜输出为证据。 +7. **Commit 格式**:`type(scope): description`,一次提交一个变更集。 +8. **新 feature 模式**:在 `feature//` 下用 `mcl.android.feature` 插件,暴露 `Screen` 组合函数 + `_ROUTE` 路由常量 + `ViewModel`,并在 `app` 的 `AppNavHost` 注册路由。 + +## 文档地图 + +| 主题 | 文档 | +|------|------| +| 架构与模块依赖 | [docs/architecture.md](docs/architecture.md) | +| 编码规范 / 命名 / commit | [docs/conventions.md](docs/conventions.md) | +| 测试策略与命令 | [docs/testing.md](docs/testing.md) | +| 构建 / 签名 / 发布 | [docs/deployment.md](docs/deployment.md) | +| 工作状态与遗留项 | [docs/progress.md](docs/progress.md) | +| 架构决策记录(ADR) | [docs/decisions/](docs/decisions/) | +| 约定插件设计说明 | [build-logic/convention/DESIGN_NOTES.md](build-logic/convention/DESIGN_NOTES.md) | +| domain 层设计说明 | [core/domain/DESIGN_NOTES.md](core/domain/DESIGN_NOTES.md) | + +## 常见任务 + +| 任务 | 做法 | +|------|------| +| 新增一个 feature 模块 | 建 `feature//` + `mcl.android.feature` + Screen/ViewModel,在 `RootPlugin.kt` 与 `settings.gradle.kts` 注册,更新 `docs/architecture.md` | +| 新增一个依赖 | 加进 `gradle/libs.versions.toml`,模块里用 `libs.xxx` 引用 | +| 修改模块依赖关系 | 改对应模块的 `build.gradle.kts`,跑 `./gradlew verifyArchitecture` 确认不违规 | +| 本地完整验证 | `bash scripts/verify.sh` | diff --git a/README.md b/README.md new file mode 100644 index 0000000..9465f84 --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# 明缘进阶场 Android 客户端 + +「明缘进阶场」的 Android 客户端,与 `wechat-backend`(明缘进阶场 API)配套。采用 Kotlin + Jetpack Compose + Hilt + Retrofit + Room + DataStore + Navigation 的现代多模块架构,参考 Google Now in Android 官方示例。 + +## 项目状态 + +当前处于**基础建设阶段**:多模块骨架、约定插件、版本目录、验证脚本与 CI 均已就绪;各 feature 模块为空占位,尚未接入真实后端。详见 [docs/progress.md](docs/progress.md)。 + +## 快速开始 + +```bash +./gradlew :app:assembleDebug # 构建调试 APK +./gradlew test testDebugUnitTest # 运行单元测试 +bash scripts/verify.sh # 完整验证 +``` + +## 技术栈 + +- **UI**:Jetpack Compose + Material 3 +- **架构**:多模块(app / core / feature)、MVVM、单向数据流(StateFlow) +- **依赖注入**:Hilt(KSP) +- **网络**:Retrofit + OkHttp + kotlinx-serialization +- **本地存储**:Room + DataStore +- **导航**:Navigation Compose +- **构建**:Gradle 约定插件(build-logic)+ 版本目录(version catalog) +- **质量**:Spotless/ktlint + Android Lint + 自定义架构检查 + +## 文档 + +架构、规范、测试、部署与决策记录见 [AGENTS.md](AGENTS.md) 的文档地图,或直接浏览 [docs/](docs/)。 diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..b74df64 --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,69 @@ +# 架构 + +## 概述 + +本项目采用 Google Now in Android(NIA)风格的多模块架构,将应用按**职责边界**拆分为 `app`、`core/*`、`feature/*` 三类模块。核心目标是让依赖方向**严格单向**、让每一层**可独立编译与测试**、让领域逻辑**与 Android 框架解耦**。 + +## 模块结构 + +| 模块 | 类型 | 职责 | +|------|------|------| +| `app` | application + compose + hilt | 组合根:`@HiltAndroidApp` Application、`MainActivity`、单一 `AppNavHost` | +| `core:common` | 纯 JVM | 无 Android 依赖的通用工具 | +| `core:model` | 纯 JVM | 领域模型(跨层共享的不可变数据) | +| `core:domain` | 纯 JVM | UseCase + Repository 接口(业务规则,框架无关) | +| `core:data` | android library + hilt | Repository 实现,编排 network/database/datastore | +| `core:network` | android library + serialization | Retrofit/OkHttp、DTO、序列化 | +| `core:database` | android library + room | Room 实体 / DAO / 数据库 | +| `core:datastore` | android library | DataStore 偏好存储 | +| `core:designsystem` | android library + compose | 主题 / 颜色 / 字体 / 通用组件 | +| `core:ui` | android library + compose + hilt | 共享 Compose UI 与导航辅助 | +| `feature:*` | feature | 各业务功能(auth/checkin/plan/course/survey/feedback/profile) | + +## 分层与依赖方向 + +依赖必须**单向向下**,杜绝环。完整 allow-list 编码在 `build-logic/convention/src/main/kotlin/RootPlugin.kt` 的 `verifyArchitecture` 任务中——任何越层依赖都会在 `./gradlew verifyArchitecture` 时失败。 + +``` +app ────────────► 所有模块(组合根) +feature/* ──────► core:ui, core:designsystem, core:domain, core:model +core:data ──────► core:domain, core:model, core:network, core:database, core:datastore +core:network ───► core:common, core:model +core:database ──► core:model +core:datastore ─► core:model +core:ui ────────► core:designsystem, core:model +core:designsystem ► core:common +core:model, core:common ► (叶子,无项目依赖) +``` + +分层语义(自上而下): + +- **UI 层**(`feature/*` + `core:ui` + `core:designsystem`):渲染界面、处理交互、持有 `ViewModel`。 +- **领域层**(`core:domain` + `core:model`):业务规则(UseCase)、领域模型、Repository 接口。**纯 Kotlin,不依赖 Android**,可快速跑在 JVM 上测试。 +- **数据层**(`core:data` + `core:network`/`database`/`datastore`):Repository 实现、网络与本地数据源,依赖领域层接口。 + +## 模块配置(约定插件) + +每个模块的 `build.gradle.kts` 只声明 `mcl.*` 约定插件 + namespace,不手写模板。插件语义: + +| 插件 | 适用 | +|------|------| +| `mcl.kotlin.library` | 纯 JVM 模块(common/model/domain) | +| `mcl.android.library` | Android 库(network/database/datastore/data) | +| `mcl.android.library.compose` | Android 库 + Compose(designsystem/ui) | +| `mcl.android.feature` | feature 模块(= library.compose + hilt + 导航/lifecycle 依赖) | +| `mcl.android.application.compose` | app(= application + compose) | +| `mcl.android.hilt` | Hilt(KSP) | +| `mcl.android.room` | Room(KSP) | +| `mcl.android.lint` | Android Lint 配置 | + +## 新增一个模块 + +1. 建目录 + `build.gradle.kts`(选对 `mcl.*` 插件 + 设 namespace)+ 空 `AndroidManifest.xml`(仅 Android 模块)。 +2. 在 `settings.gradle.kts` 里 `include(":...")`。 +3. 在 `RootPlugin.kt` 的 allow-list 里加一条(模块路径 → 允许依赖的模块)。 +4. 更新本文件的分层图。 + +## 组合根(app) + +`app` 负责把各模块**装配**起来:`MingyuanApplication` 标注 `@HiltAndroidApp`;`AppNavHost` 用 Navigation Compose 把各 feature 的路由常量(`_ROUTE`)与 Screen 组合成单一导航图。后续接入数据层时,`app` 会直接依赖 `core:data`(以及含 Hilt `@Module` 的 network/database/datastore)完成 DI 聚合。 diff --git a/docs/conventions.md b/docs/conventions.md new file mode 100644 index 0000000..a892baa --- /dev/null +++ b/docs/conventions.md @@ -0,0 +1,36 @@ +# 编码规范 + +## Kotlin + +- 遵循官方 Kotlin 风格:四空格缩进、类型 `UpperCamelCase`、成员 `lowerCamelCase`、常量 `SCREAMING_SNAKE_CASE`。 +- 优先不可变:用 `val`、`data class` 的 `copy()`,避免可变共享状态。 +- 严格空安全,不用 `!!` 除非有充分理由。 +- 用密封类(sealed class)表达状态 / 事件 / 结果。 +- 协程:数据操作在 `Dispatchers.IO`,UI 在 `Dispatchers.Main`;用 `StateFlow` 暴露 UI 状态(单向数据流)。 + +## Compose + +- 屏幕组合函数命名 `Screen`,接收 `modifier: Modifier = Modifier` 作为首个参数。 +- `@Composable` 函数名用 `PascalCase`;被 `@Preview` 或测试使用的纯函数可豁免下划线命名(见 `.editorconfig`)。 +- 复用组件放 `core:designsystem` 或 `core:ui`,不复制到 feature。 + +## 命名 + +- Activity/Application 以 `Activity`/`Application` 结尾;ViewModel 以 `ViewModel` 结尾;UseCase 以 `UseCase` 结尾;Repository 实现以 `RepositoryImpl` 结尾(接口在 `core:domain` 用 `Repository`)。 +- 资源用 `snake_case`;字符串、颜色、尺寸统一在 `core:designsystem` 管理。 +- 测试类以 `Test` 结尾。 + +## 版本与依赖 + +- 依赖版本只写在 `gradle/libs.versions.toml`,模块里用 `libs.*` 访问器引用,禁止硬编码版本。 +- 新增依赖先确认是否已有等价项,避免重复引入。 + +## Git + +- Commit 格式 `type(scope): description`,type 如 `feat`/`fix`/`refactor`/`docs`/`build`/`test`/`chore`;一次提交一个变更集。 +- 提交前跑 `./gradlew spotlessApply` 与 `bash scripts/verify.sh`。 + +## 文档 + +- 重要决策写入 `docs/decisions/`(ADR),行为与文档在同一次变更里一起更新。 +- 模块级"为什么这样设计"写在该目录的 `DESIGN_NOTES.md`(即时上下文)。 diff --git a/docs/decisions/0001-multi-module-nia.md b/docs/decisions/0001-multi-module-nia.md new file mode 100644 index 0000000..3e995cc --- /dev/null +++ b/docs/decisions/0001-multi-module-nia.md @@ -0,0 +1,22 @@ +# ADR 0001:多模块 NIA 架构,feature 单模块(不做 api/impl 拆分) + +- **状态**:已接受 +- **日期**:2026-09-03 + +## 背景 + +项目从单模块 Compose 模板起步,需要确立可扩展、可测试、可维护的架构基础。 + +## 决策 + +采用 Google Now in Android(NIA)风格多模块架构:`app`(组合根)+ `core/*`(共享能力)+ `feature/*`(业务功能)。每个 feature 用**单模块**,不做 nowinandroid 的 `feature:*/api` + `feature:*/impl` 拆分。 + +## 理由 + +- 依赖方向单向、职责清晰,符合参考项目 `nowinandroid` 与 `app_device` 的共识。 +- 团队规模小、feature 间共享少,api/impl 拆分会带来额外模块与样板,收益有限。 + +## 后果 + +- 约定插件 `mcl.android.feature` 预置 feature 通用依赖,新增 feature 只需一个插件声明。 +- 若未来 feature 间需要稳定 API 边界,再引入 api/impl 拆分(成本可控)。 diff --git a/docs/decisions/0002-convention-plugins-and-version-catalog.md b/docs/decisions/0002-convention-plugins-and-version-catalog.md new file mode 100644 index 0000000..db292b5 --- /dev/null +++ b/docs/decisions/0002-convention-plugins-and-version-catalog.md @@ -0,0 +1,23 @@ +# ADR 0002:约定插件 + 版本目录作为唯一配置事实源 + +- **状态**:已接受 +- **日期**:2026-09-03 + +## 背景 + +多模块项目若在每个模块手写 `android {}` 模板,会出现配置漂移、版本不一致、样板重复。 + +## 决策 + +- **约定插件**(`build-logic/`,included build):提供 `mcl.*` 插件,集中编码 compileSdk/minSdk、Java/Kotlin target、Compose、Hilt、Room、Lint、Spotless 等配置。模块只声明插件 + namespace。 +- **版本目录**(`gradle/libs.versions.toml`):所有依赖版本唯一事实源,主构建与 build-logic 共用。 + +## 理由 + +- 把"严谨性"编码进确定性规则(harness-engineering 的"严谨性转移"原则),而非依赖人工记忆。 +- 一个版本改一处,全项目生效。 + +## 后果 + +- 新增/调整模块配置需改 build-logic;新增依赖需改版本目录。 +- 好处:`verifyArchitecture` 等约束可挂在约定插件体系里统一执行。 diff --git a/docs/decisions/0003-toolchain-and-versions.md b/docs/decisions/0003-toolchain-and-versions.md new file mode 100644 index 0000000..f988595 --- /dev/null +++ b/docs/decisions/0003-toolchain-and-versions.md @@ -0,0 +1,23 @@ +# ADR 0003:工具链与版本选型 + +- **状态**:已接受 +- **日期**:2026-09-03 + +## 背景 + +需要为一套现代化 Android 技术栈锁定版本。项目原本是 AGP 9.4.0 + Kotlin 2.2.10 + Gradle 9.6.0。 + +## 决策 + +- **保留**:Gradle 9.6.0(wrapper)、AGP 9.4.0、compileSdk/targetSdk 37、minSdk 28、Compose BOM 2026.02.01。 +- **对齐 nowinandroid 已验证矩阵**:Kotlin 2.3.0、KSP 2.3.4、Hilt 2.59、Room 2.8.3、Retrofit 2.11.0、OkHttp 4.12.0、kotlinx-coroutines 1.10.1、kotlinx-serialization 1.8.0、Navigation Compose 2.8.5、DataStore 1.2.0、Lifecycle 2.10.0、truth 1.4.4、turbine 1.2.0、spotless 8.3.0、ktlint 1.4.0。 + +## 理由 + +- 采用 AGP 9 内置 Kotlin(不显式应用 `org.jetbrains.kotlin.android`),与项目原有模板一致,且经 nowinandroid 验证可与 KSP/Hilt/Room 共存。 +- Kotlin 升到 2.3.0 是为了直接复用 nowinandroid 已证明可用的 KSP 2.3.4 / Hilt 2.59 / Room 2.8.3 组合,避免为旧 Kotlin 猜测 KSP 补丁版本。 + +## 后果 + +- 全量构建已验证通过(`:app:assembleDebug`)。 +- 若未来 AGP 升级改变内置 Kotlin 行为,需回归验证 KSP/Hilt/Room 兼容性。 diff --git a/docs/decisions/0004-package-name.md b/docs/decisions/0004-package-name.md new file mode 100644 index 0000000..6803f57 --- /dev/null +++ b/docs/decisions/0004-package-name.md @@ -0,0 +1,22 @@ +# ADR 0004:包名与显示名 + +- **状态**:已接受 +- **日期**:2026-09-03 + +## 背景 + +项目原有包名 `com.mingyuan.android_client`(applicationId 与 namespace),显示名 `android-client`。 + +## 决策 + +- **保留包名** `com.mingyuan.android_client`(避免牵连改动),仅把显示名 `app_name` 改为「明缘进阶场」。 +- 模块命名空间沿用 `com.mingyuan.android_client.<路径点分>`。 + +## 理由 + +- 重命名 applicationId/namespace 会牵连 manifest、包目录、DI 组件等;基础建设阶段优先把架构骨架跑通。 +- 下划线命名虽不符合惯例,但不影响构建与运行,作为已知小问题保留。 + +## 后果 + +- 显示名已是「明缘进阶场」;包名中的下划线如需规范化,可在后续单独决策并一次性迁移。 diff --git a/docs/deployment.md b/docs/deployment.md new file mode 100644 index 0000000..c8d319b --- /dev/null +++ b/docs/deployment.md @@ -0,0 +1,27 @@ +# 构建与部署 + +## 构建 + +```bash +./gradlew :app:assembleDebug # 调试 APK(app/build/outputs/apk/debug/) +./gradlew :app:assembleRelease # 发布 APK(未启用混淆,签名见下) +./gradlew installDebug # 安装到已连接设备 +``` + +## 签名 + +当前发布构建**未配置签名**,也**未启用 R8 混淆**(`minify` 关闭)。接入真实发布前需: + +1. 生成/配置 keystore(`keystore.properties` 或环境变量)。 +2. 在 `app/build.gradle.kts` 配置 `signingConfigs` 与 `release` buildType 的签名。 +3. 需要时启用 R8(`isMinifyEnabled = true` + proguard 规则)。 + +## 环境 + +- JDK 17+(`settings.gradle.kts` 有版本检查)。 +- Android SDK 路径见 `local.properties`(`sdk.dir`)。 +- `compileSdk` 37 / `minSdk` 28 / `targetSdk` 37。 + +## CI + +`.github/workflows/ci.yml` 在推送 GitHub 后自动运行 `spotlessCheck → test/testDebugUnitTest → :app:assembleDebug → verifyArchitecture → lintDebug`。 diff --git a/docs/progress.md b/docs/progress.md new file mode 100644 index 0000000..2b1ca61 --- /dev/null +++ b/docs/progress.md @@ -0,0 +1,35 @@ +# 工作状态 + +> 本文件是跨会话的工作状态快照。变更后请更新。 + +## 当前状态 + +- ✅ 基础建设:多模块骨架(9 core + 7 feature + app)、约定插件(`mcl.*`)、版本目录、`verifyArchitecture`、Spotless/Lint、验证脚本、CI、文档、DESIGN_NOTES。 +- ✅ 验证通过:`./gradlew :app:assembleDebug`、`test`/`testDebugUnitTest`、`verifyArchitecture` 全绿。 +- ⬜ 各 feature 为占位屏(`Screen` + 空 `ViewModel`),无业务逻辑。 + +## 已完成 + +- [x] build-logic 约定插件 + 版本目录 + 根配置 +- [x] 17 个模块骨架 + app 组合根(Hilt + Navigation) +- [x] 主题迁入 `core:designsystem`(`MingyuanTheme`) +- [x] `verifyArchitecture` 架构检查任务 +- [x] AGENTS.md / README / docs/* / DESIGN_NOTES +- [x] scripts/verify.sh + CI workflow + +## 已阻塞 + +- 无。 + +## 下一步计划 + +1. 接入真实后端:`core:network` 定义 Retrofit API + DTO(对应 wechat-backend 的 `/api/v1/*`),`core:data` 实现 Repository,`app` 补齐 DI 聚合。 +2. `core:database`/`core:datastore` 落地本地存储 schema。 +3. 逐个 feature 实现业务逻辑(checkin/plan/course/survey/feedback/profile/auth)。 +4. 发布前:签名 + R8 混淆 + 图标/品牌。 + +## 遗留项(暂缓) + +- Roborazzi 截图测试、WorkManager、Gradle Managed Devices —— 有真实 UI/后台任务时再接入。 +- `docs/api.md`、`docs/data-model.md` —— 后端接线时再建。 +- app 尚未直接依赖 `core:network`/`core:database`/`core:datastore`;接入 DI 时补上(已在 `verifyArchitecture` allow-list 中放行)。 diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 0000000..2de7f15 --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,34 @@ +# 测试 + +## 运行命令 + +```bash +./gradlew test # 纯 JVM 模块(core:model/domain/common)单元测试 +./gradlew testDebugUnitTest # Android 模块单元测试(本地 JVM) +./gradlew connectedDebugAndroidTest # 仪器测试(需连接模拟器/真机) +``` + +## 目录与命名 + +- JVM 单元测试:`src/test/kotlin/`;Android 单元测试:`src/test/kotlin/`。 +- 仪器测试:`src/androidTest/kotlin/`。 +- 测试类命名 `Test`;测试方法名用反引号描述意图(`fun \`invoke delegates to implementation\`()`)。 + +## 测什么 + +- **领域层优先**(`core:domain`):UseCase、纯逻辑、状态转换——用 `kotlinx-coroutines-test` 的 `runTest` + `truth` 断言,快速且稳定。 +- **数据层**(`core:data`):Repository 用 fake 数据源(实现 `core:domain` 里的接口),不真打网络/数据库。 +- **UI**:`@HiltViewModel` 的 `ViewModel` 用 `StateFlow` 断言状态;Composable 用 Compose UI test。 +- 网络 / 数据库边界用接口隔离,用 fake/dummy 替换真实实现。 + +## 依赖 + +- `junit`:`@Test` +- `truth`:断言(更可读的 `assertThat(...)`) +- `turbine`:Flow 测试(需要时再加) +- `kotlinx-coroutines-test`:`runTest` + +## 约定 + +- 不写"假测试"(无断言的空壳);每条测试验证一个明确行为。 +- 提交 PR 时在描述里写清运行的命令与结果。