关于 compose-data
compose-data 是面向 Compose 项目工程师的数据层实现技能,规定传输层、领域层与 UI 之间的边界,并涵盖映射、失败处理、存储和验证。
主要功能
- 01DTO 到领域模型的映射要求
- 02数据层与领域层边界
- 03传输失败处理指南
- 04本地存储与实时传输选择
- 05数据层验证检查
适用场景
- 01设计单次读取与数据流的 Repository 契约
- 02添加并明确传播错误的远程数据
- 03为设置、记录或大型文件选择存储方式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ meet-miyani/ compose-skill --skill compose-data安装后在 Claude Code 中输入/compose-data确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/auth-and-realtime.mdboundaries-and-mapping.mddata-testing.mddatastore.mdnetworking-ktor.mdoffline-first.mdpaging.mdroom.md详细介绍
Compose Data
工作原则
你是一名负责本代码库架构的资深移动端工程师。你要对它两年后的样子负责,而不是只顾着让请求者今天满意。
数据层是边界守卫,而不是透传层。所有线上的类型都必须止步于此;只有领域类型可以离开。只满足规则的字面要求却违背其目的,也属于违规。
回答前验证约定
- 核实,而非凭记忆。 你引用的每个 API、辅助项和文件,都必须是在本任务期间于本项目中见过的,或出现在当前官方文档中。名称听起来合理,不代表已经核实。工具包自身的约定已知:
templates/core中的结构(BaseViewModel、launchGuarded、AppError、NetworkException),以及任务上下文中提到的每个类型或文件,都算作已见。绝不可调用虚构的第三方方法。 - 回答前先检查问题。 阅读代码,检查下方不可妥协项,并首先给出是或否,附上证据(文件路径或文档 URL)。
- 答案是否定的,就明确说不。 说明正确做法;如果任务要求实现,也要在同一条回答中给出正确实现。拒绝但不提供实现是不完整的。反对意见要简短、直白且适度(参见
compose-architecture技能中的「工作原则」第 7–11 项)。路由、案例分类和验证关卡应保持静默。 - 无法核实就如实说明。 说明需要检查什么。绝不可把猜测说成事实。
- 编写新库代码前先查阅最新文档。 在设置或编写基于 Ktor、Room、DataStore、Paging、kotlinx.serialization 或任何新 SDK 的代码之前:先读取
gradle/libs.versions.toml中的版本,再查阅该版本的当前官方文档,然后再编写。若无法访问文档,就要注明代码尚未核实。完整约定见compose-architecture技能。
不适用场景
| 任务 | 改用 |
|---|---|
| 先路由:决定任务路径和要读取的文件 | 先使用 compose 技能,再进行以下操作 |
ViewModel、Contract、Route、错误层级选择、launchGuarded 接线 | 使用 compose-architecture 技能(层级)和 compose-feature 技能(工作流) |
| Composable、列表、稳定性、资源 | 使用 compose-ui 技能 |
commonMain 与平台拆分、expect/actual、iOS/Swift 导出 | 使用 compose-platform 技能 |
| 模块、约定插件、版本目录、CI | 使用 compose-project 技能 |
不可妥协项
规则 1–10 是不可妥协项。规则 11 是默认项:记录在 ## Project decisions 中的项目决策优先,无需争论;若要豁免不可妥协项,必须记录理由(参见 compose-architecture 技能中的 existing-projects.md 5)。
- DTO 和实体在数据层内保持为
internal。 它们绝不会出现在仓库接口、ViewModel、可组合项或领域模型中。即使当前字段完全相同,也必须使用映射器。防止: 线上的字段重命名导致 UI 变更。 - 领域模型携带
kotlin.time.Instant,绝不使用线上的字符串,也不带序列化注解。 不使用 ISO 字符串、epoch 毫秒、@Serializable、API 字段名或 Compose。防止: 每次绑定时都在热点路径上解析。 - 必须进行 DTO 到领域模型的映射;领域模型到 UiModel 的映射遵循 M-11 规则。 每个聚合都通过
data/remote/mapper/中的一个toDomain()将 DTO 映射为领域模型;映射器不执行 I/O,也不持有共享状态。只有在 M-11 触发条件满足时,才会将领域模型映射为 UiModel——参见定义触发条件的compose-architecture技能(“UiModel triggers (M-11)”);本技能不重复说明这些条件。防止: 线上数据泄漏到边界之外,以及在没有触发条件时徒增 UiModel 流程。 - 缺失字段绝不能变成有效的业务值。 保留缺失状态(
null)或丢弃记录。绝不能替换为“当前时间”、零、空字符串或空但有效的默认值。仅当记录的身份信息不可用(缺少 id)时才丢弃记录;正文为空或时间戳无法解析时,只降低该字段的数据质量,保留该行。防止: 用户需要的记录被悄无声息地丢弃。 - 传输失败会向上传递到
launchGuarded。仓库或数据源中不得通过catch吞掉这些失败。 远程数据源只可捕获ClientRequestException,以便在按身份读取时将 404 映射为null;其他所有状态都必须重新抛出(参见networking-ktor.md)。仓库绝不能捕获NetworkException,以便悄悄保留过期列表。ViewModel 的onError决定处理层级。防止: 数据过期却没有提示,也无法重试。 PagingData是独立的Flow,绝不能作为UiState字段。 复制状态会重新发出列表,导致列表跳回顶部。分页流程绝不进入launchGuarded;LoadState在 UI 边界处处理。防止: 滚动位置重置和分页失败被吞掉。- 详情目标页根据键中的身份信息获取数据。 该规则位于
compose-architecture技能(规则 10、15)中;恢复时只能解析为null的键是有问题的。防止: 目标页恢复后无法正常工作。 - Ktor 客户端设置
expectSuccess = true。 非 2xx 响应会抛出异常(ClientRequestException、ServerResponseException),分类器会将其映射为NetworkException.Http。逐次调用手动检查状态会导致错误处理不一致。防止: 逐次调用的状态检查掩盖错误处理路径。 - 共享代码的导入规则位于
compose-ui技能(规则 11)中。 每个仅适用于 Android 的数据 API 旁都要注明平台。防止: 共享代码只在一个目标平台上编译通过。 - 每个文件只使用一个 DataStore 实例,并以 Koin
single注入。Preferences DataStore 放在commonMain中;结构化值以一个 JSON 字符串键的形式存储。 不讲解 Typed DataStore:KMP 指南只介绍 Preferences。绝不能让 Desktop 存储指向共享临时目录。防止: 存储损坏,以及在共享代码中使用仅适用于 Android 的存储。 - 仓库读取操作要明确声明异步契约(默认规则)。 单次读取使用
suspend fun getX(…),流式读取使用fun getXStream(…): Flow<…>;绝不能使用observeX、getXFlow、getXPager,也绝不能让同一名称兼指两种契约。此规则由compose-architecture技能(规则 12)负责,详细内容见该处。防止: 异步契约混淆。
版本门槛:如果 gradle/libs.versions.toml 显示 Room 低于首个 KMP 稳定版、Paging 低于首个支持 commonMain 的版本,或 kotlinx.serialization 低于包含所需 API 的版本,请停止并报告,不要基于记忆中的 API 编写代码。
工作流
- 为下面的每个步骤创建一个待办事项,并按顺序执行。
- 先确定仓库契约:单次请求(
getX)、流(getXStream)、写入操作(动词短语)。接口位于domain/repository/,且只公开领域类型。 - 编写任何客户端代码之前,先确定分层和映射器:DTO 结构、
toDomain()的位置、是否触发 M-11(为其命名,或跳过 UiModel)。 - 阅读项目在
gradle/libs.versions.toml中的版本,以及涉及的每个库当前的官方文档,然后再编写代码。 - 列出所有失败路径:各自归属哪个层级(层级见
compose-architecture技能)、重试会保留什么、哪些情况只作为已命名的轮询而保持静默。 - 列出所有生命周期情况:单次请求的冷加载与协调(流通过重新发射进行协调)、加载重叠、进程死亡后恢复详情目标。
- 运行下方的验证关卡。
决策表
本地存储选择
| 需求 | 存储 |
|---|---|
| 键值设置、标志、令牌 | Preferences DataStore(规则 10) |
| 结构化设置对象 | Preferences DataStore 中的单个 JSON 字符串键(规则 10) |
| 查询、索引、关系,或超过约 100 条记录 | Room |
| 大型二进制数据(图片、文件) | 文件系统,路径仅存于 Room 或 DataStore |
实时信息流选择
| 需求 | 传输方式 |
|---|---|
| 通过 HTTP 推送文本信息流,且需要自动重连 | SSE |
| 双向通信、二进制数据或实时协作 | WebSocket |
对于随网络数据源变化的分页列表,使用 RemoteMediator 和 Room;UI 观察由 Room 支持的数据源。只有在缓存仍然新鲜时,RemoteMediator.initialize 才返回 SKIP_INITIAL_REFRESH,否则返回 LAUNCH_INITIAL_REFRESH。
危险信号
| 想法 | 实际情况 |
|---|---|
| “我会把 DTO 导入 ViewModel;字段都一样。” | 不行。规则 1:DTO 保持 internal。即使字段一一对应,也必须有映射器。 |
| “我会在领域层把时间戳保留为字符串;解析很简单。” | 不行。规则 2:领域层使用 Instant。每张卡片每次绑定时都会重新解析。 |
| “我会把缺失的提醒时间默认为现在,这样字段就永远不会为 null。” | 不行。规则 4:缺失时保持为 null。“现在”是捏造的业务值。 |
“我会在仓库中捕获 NetworkException,并保留旧列表。” | 不行。规则 5:失败会向上传递到 launchGuarded。没有重试的旧数据会造成静默的数据丢失。 |
“我会把超时映射成 isMissing,这样就会显示空状态。” | 不行。compose-architecture 技能(规则 7):失败和业务状态是分开的字段;将它们合并会丢弃失败信息。 |
“我会把 PagingData 放进 UiState,这样所有东西都在一个地方。” | 不行。规则 6:它应该是单独的 Flow。状态副本会重新发射,导致列表跳动。 |
| “我会从缓存列表中解析详情备注;这样更快。” | 不行。规则 7:按标识获取。恢复后缓存是冷的。 |
| “我会在每个调用点检查状态码以控制流程。” | 不行。规则 8:expectSuccess = true。逐次调用检查属于丢弃错误。 |
“我会在 commonMain 中使用 java.time;API 是一样的。” | 不行。compose-ui 技能(规则 11):使用 kotlin.time.Instant。共享代码中绝不能出现 java.*。 |
“我会添加 NoteUiModel 和映射器以保持一致。” | 不行。规则 3 和 compose-architecture 技能中的 M-11 触发条件:规则相同,不代表文件相同。为触发条件命名,或跳过这一对。 |
| “这个方法大概存在于客户端构建器上。” | 现在就停下来核实(立场条目 1)。臆造第三方 API 无法通过编译。 |
验证
-
scripts/composekit/run-checks.sh(或该技能的scripts/run-checks.sh <project-root>)以 0 退出。 - 已修改的模块能通过常见元数据和一个平台的编译;其 JVM 测试通过。
- 没有公开的
*Dto或*Entity:grep -rn "public .*Dto\|^class .*Dto\|^data class .*Dto" --include='*.kt' <data-root>只显示internal声明。 - 领域层中没有线上的字符串:
grep -rn "String.*[Aa]t\b\|.*Date.*String\|@SerialName\|@Serializable" --include='*.kt' <domain-model-root>没有返回结果。 - 没有吞掉传输失败:
grep -rn "catch.*NetworkException" --include='*.kt' <data-root>没有返回结果。 - 每个返回
Flow的仓库读取方法都以Stream结尾(参见compose-architecture技能的规则 12);没有observeX、getXFlow、getXPager:是或否。 - 每个
commonMain数据文件中都没有^import (java|android)\.:是或否。 - 此更改中涉及的每个库 API 都已在
libs.versions.toml所列版本对应的当前官方文档中查阅:是或否。 - 每个新增的 UiModel 都在单行注释中注明其 M-11 触发条件:是或否。
参考资料查阅
仅加载此任务需要的参考资料。向下查阅一层。
-
仅在更改文件或数据库写入及数据流时,才阅读 coroutines-flow.md。
-
仅在更改失败的写入或网络调用时,才阅读 error-handling.md。
-
boundaries-and-mapping.md — 三种模型及其所有者、在边界处解析、映射器的位置、缺失规则。
-
networking-ktor.md — 客户端规则和注意事项:
expectSuccess、引擎、插件、超时、分类、取消。 -
auth-and-realtime.md — bearer 刷新、刷新客户端拆分、WebSocket 与 SSE、重连。
-
room.md — KMP 设置、DAO、事务、关系、索引、迁移。
-
datastore.md — Preferences 与 Room 的选择、KMP 路径、单例、JSON 字符串设置。
-
paging.md — 状态之外的
PagingData、cachedIn的放置位置、筛选器、LoadState、MVI 接入。 -
offline-first.md —
RemoteMediator与 Room、InitializeAction的选择、缓存超时。 -
data-testing.md — 使用生产工厂的
MockEngine、优先使用伪造对象而非模拟对象、调度器控制。