本文件适用于整个仓库。目标是在保持 Android Compose 脚手架简洁的前提下,完成可审查、范围明确的改动。
- 默认使用中文解释、总结和编写代码注释。
- 代码标识符、命令、路径、接口字段、日志与报错原文保持原样。
- 先阅读相关实现、调用方和现有测试,再修改代码;不要只依据 README 或本文件推断实现。
- 用户在当前会话中的明确要求优先于本文件;子目录 AGENTS.md 在其目录内补充或覆盖根目录规则,但不得覆盖更高优先级指令。
- 默认不执行 Test Plan,不运行构建、Detekt、单元测试、设备测试或自动格式化。改动后仅执行以下验证命令:
git diff --check- 上述命令仅检查差异中的空白等问题,不证明代码能编译或功能正确。
- 收尾说明修改内容、实际执行的验证及未验证事项,禁止把未执行的检查写成通过。
- 只有用户明确要求扩大验证范围时,才运行对应命令;无需对已经授权的检查重复请示。
- 同一错误连续出现两次后,停止重复尝试;联网检索 3–5 种可能方案,优先核对官方资料,再选择符合当前授权和工具链约束的方案。不得上传凭据或敏感日志。
- 本次改动新增或修改了
src/test下的测试文件时,运行对应模块的testDebugUnitTest属于已授权的最低验证,无需逐次请示。
以下为参考命令,不是默认执行指令:
# 当前 CI 的验证命令
./gradlew assembleDebug assembleRelease detekt testDebugUnitTest --console=plain
# 用户明确授权时,可按影响范围运行
./gradlew :feature:cart:testDebugUnitTest --console=plain
./gradlew detekt --console=plain命令应在仓库根目录执行。不得使用会掩盖退出码的管道;以原始命令退出码判断成功与否。
这是 Android 原生项目,不是 Kotlin Multiplatform 项目。不要因姐妹项目或遗留注释引入跨平台抽象。
当前为五 Tab 脚手架:
- 首页、逛、消息、购物车、我的各自持有返回栈。
- 登录是根级全屏占位流程,尚未实现真实认证。
- 购物车使用 wanandroid 文章分页演示选择、刷新、分页和金额展示,不是真实交易业务。
以以下配置作为事实来源:
- 模块注册:settings.gradle.kts。
- 依赖与插件版本:gradle/libs.versions.toml。
- Gradle Wrapper:gradle/wrapper/gradle-wrapper.properties。
- Daemon JDK:gradle/gradle-daemon-jvm.properties。
- 构建行为:根目录及各模块 build.gradle.kts。
- CI:.github/workflows/ci.yml。
- 格式与静态规则:.editorconfig、config/detekt/detekt.yml。
当前配置声明 AGP 9.3.2、Kotlin 插件版本 2.2.10、Compose BOM 2026.08.00; compileSdk/targetSdk 为 37,minSdk 为 24。 Gradle Daemon 使用 JDK 22;Java 编译目标为 11,Detekt jvmTarget 为 21。 这些属于不同配置,不要混为一谈。
保持现有 built-in Kotlin 配置,不自行添加 org.jetbrains.kotlin.android 或 kapt。 版本目录中的 Kotlin 版本、stdlib 强制版本不能单独证明实际编译器版本。 不要把历史兼容问题写成永久结论,例如“JDK 22 是所有情况下的上限”。 依赖调整前核对实际工具链、插件和元数据兼容性,不盲目升级。
| 模块 | 职责 |
|---|---|
| app | 应用入口、五 Tab 壳、根导航、跨 Feature 回调连接、Hilt 组合根、Room 聚合、发布配置 |
| core:common | AppConfig、AppLogger 等无 UI 基础能力;当前是 Android library,依赖 Kermit |
| core:model | Article 等共享数据模型、Money、NetworkResult;当前使用 kotlinx.serialization,无 UI、网络客户端和数据库框架依赖 |
| core:data | API、Repository 契约与实现、网络、存储、调度器及数据层 DI |
| core:player | 通用媒体播放接口、Media3 实现、缓存、视频输出与系统会话 |
| core:design | 主题、图片入口、通用 Compose 组件、分页状态机和 Nav3 容器 |
| feature:* | 页面展示、页面状态、用户 Intent、ViewModel、路由及 EntryProvider |
| baselineprofile | 保留性能生成器源码;插件目前停用 |
依赖方向:
- app 按需依赖 feature 和 core。
- feature 按需依赖 core,禁止 Feature 之间直接依赖。
- core:data 和 core:design 可依赖 core:model、core:common,彼此保持独立。
- core:player 仅依赖 core:common;网络客户端由 app 注入,不依赖业务模型或数据层。
- core:model、core:common 不依赖其他项目模块。
- 禁止 core 反向依赖 app 或 feature。
core:data 的 api/、repository/、di/ 当前按业务前缀扁平组织,例如 CartApi、CartRepository。 先沿用现有结构;出现明确的职责或规模问题时再提出调整。
当前 AppDatabase 只有占位 Entity,没有业务 DAO。 新增业务 Entity/DAO 按数据层职责放入 core:data,由 app 聚合;届时同步处理所需依赖、处理器和迁移,不要依据旧注释放进 feature/data。
- 有业务状态的页面沿用不可变 UiState、sealed Intent 和 onIntent() 入口。
- 静态占位页不必创建无意义的 ViewModel、Intent 或 UseCase。
- Screen 可获取 ViewModel、收集状态和处理导航事件;Content 与可复用组件接收状态及回调。
- 首次加载由 ViewModel 初始化负责;用户刷新、重试和翻页通过 Intent 进入。
- 不在 LaunchedEffect(Unit) 中直接调用业务加载。
- 允许使用 Compose 副作用收集事件、观察滚动或执行 UI 动画;不要一概禁止 LaunchedEffect。
- 状态使用 collectAsStateWithLifecycle;一次性界面事件沿用 ObserveAsEvents,并评估后台期间是否允许丢失。
- 必须在返回前台后继续成立的信息应表达为状态,不能仅依赖不重放的事件。
- ViewModel 生命周期由导航作用域决定;不要声称 init 能保证整个进程只加载一次。
真实参考:CartViewModel.kt、CartScreen.kt。
- core:model 不放选中、展开、加载中等 UI 状态。
- 页面状态优先组合已有实体,不机械复制 DTO 字段。
- 允许为实际的业务语义转换、协议隔离或多数据源整合增加映射;禁止空转映射。
- 简单业务直接注入 Repository,不为单行转发创建 UseCase。
- API 协议解析、服务端错误和后端页码适配放在数据层;UI 不直接依赖 Retrofit 或调用网络接口。
- 页面内部类型优先使用 internal/private,只公开必要的跨模块接口。
组合示例:
internal data class CartItemUiState(
val article: Article,
val unitPrice: Money,
val selected: Boolean = false,
)- 需要指定 IO/CPU 调度器时,使用已有 @IoDispatcher、@DefaultDispatcher 注入。
- ViewModel 的业务协程使用 viewModelScope;不要在业务类中创建无生命周期的全局作用域。
- 保留 CancellationException 的取消语义,不转换为普通失败或成功。
- 复用 LoadableController 和 LoadableLazyColumn,不复制分页状态机。
- 控制器与 Repository 使用 1 基页码,CartRepository 转换为 wanandroid 的 0 基页码。
- 修改分页时共同检查列表、下一页页码、结束状态和进行中的请求。
- 刷新失败保留旧列表时,应同时保留与之匹配的分页状态。
- Lazy 列表 key 必须稳定且唯一;区分服务端空页与客户端去重后的空列表。
- 当前控制器仍存在刷新失败后结束状态丢失、去重空页提前结束的问题,不能把现有行为当成正确规范。
- 金额使用 Money,以 Long 最小货币单位存储,通过 formatMoney 展示。
- 保留负数、币种及溢出检查,不使用 Float/Double 计算金额。
- demoUnitPrice 仅用于演示,不能作为生产定价逻辑。
- UI 文案使用资源,样式优先使用 AppTheme/MaterialTheme;保持无障碍语义和系统 Insets 处理。
- Hilt ViewModel 使用 androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel。
- 路由实现 NavKey、支持序列化,并注册对应 SerializersModule 和 EntryProvider。
- 当前导航容器显式使用 key.toString() 作为 contentKey,因此 data object 路由必须返回带命名空间的稳定名称,例如 "CartRoute.Main"。
- 参数化路由还要评估参数和重复入栈的身份需求,不能统一返回固定名称。
- 跨 Feature 跳转由 app 连接回调,Feature 不直接操作其他 Feature 的返回栈。
- 修改导航时保留各 Tab 独立返回栈、状态恢复和 Entry 级 ViewModel 生命周期。
- 遵循 .editorconfig 和相邻代码风格,Kotlin 使用 4 空格及尾随逗号。
- 注释说明原因、约束和边界,使用中文,避免“商业级”“绝对安全”等无证据保证。
- 不为排版、命名偏好或等价写法扩大改动范围。
- 不复制当前文件中的死代码、演示逻辑或已知问题作为新代码模板。
- KeyValueStore/MMKV 只保存非敏感偏好和缓存标记。
- 必要的敏感凭据通过 SecureCredentialStore 管理,禁止明文落盘。
- 不把 Android Keystore 自动等同于所有设备都有硬件保障。
- 不为方便 JVM 测试在生产实现中增加固定密钥或静默内存降级;测试使用显式替身。
- 凭据保存失败必须可被调用方识别;当前通过 CredentialStorageException 显式抛出实现。
- 认证凭据仅发往受信任 API 范围;公共图片请求不得自动携带业务 Token。
- 图片加载器与媒体数据源统一使用 @PublicHttpClient 公共客户端,不携带业务凭据。
- 日志统一使用 AppLogger,不输出 Token、密码、签名 URL、完整请求/响应正文或未经脱敏的异常信息。
- 保持 HTTPS、禁止明文流量等已有安全配置。
仅在当前任务未包含对应授权时确认:
- 添加或升级依赖、改变工具链。
- 修改 Room Entity/Schema。
- 修改顶层 Tab、根导航拓扑或公共返回栈机制。
- 修改签名、发布或生产安全配置。
先完成只读分析,说明具体改动和影响,再请求必要决策;已有授权不重复请示。
Room Schema 变更必须规划版本递增、导出的 schema JSON 和显式 Migration。 如当前验证授权不足以生成或验证 schema,应说明限制,不伪造生成结果。 禁止使用 destructive migration 掩盖升级问题。
- 为通过检查而删除、弱化或注释失败测试,或关闭相关规则。
- 提交 local.properties、.env*、keystore、签名密码、Token 和构建产物。
- 未经要求回滚、覆盖或清理用户已有改动。
- 把尚未执行的构建、测试、设备验证或安全保证写成已完成。
- 为形式统一进行全仓迁移、机械分层或批量重写。
- 开始时检查 git status 和相关差异,明确已有改动。
- 用户要求 review 时先给发现、依据、影响和建议,不自动实施重构。
- 修改只覆盖当前任务;更新因本次改动而失真的相关文档。
- 不自动提交或推送;用户要求提交时,只暂存本次相关文件。
- Agent 创建提交时使用以下身份:
git -c user.name="local-snapshot" -c user.email="snapshot@local" commit -m "<type>: <description>"提交类型使用 feat、fix、refactor、test、docs 等,描述具体结果。 不要为了满足“干净工作区”而删除或提交用户已有文件。
收尾报告:
- 完成了什么以及为什么。
- 实际执行的验证结果。
- 未验证的行为、已知限制或需要用户处理的事项。
本文件随代码演进更新。保留能防止实际错误的规则;详细教程放 README, 审查发现放任务报告,不持续堆积到本文件。