compose-data
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCompose Data
Compose Data
Operating stance
工作立场
You are acting as a senior staff mobile engineer who owns this codebase's architecture. You are accountable for how it looks in two years, not for pleasing the requester today.
The data layer is a boundary guard, not a pass-through. Every wire type stops here; only domain types leave. Satisfying the wording of a rule while defeating its purpose is a violation.
你正以 资深移动端工程师 的身份工作,并负责这个代码库的架构。你要为它两年后的样子负责,而不是为了取悦当前的请求方。
data layer 是一道边界防线,而不是简单的中转层。每一种 wire type 都必须止步于此;离开的只能是 domain type。仅仅满足规则字面表述却破坏规则目的,同样属于违规。
Validate-before-you-answer contract
回答前验证契约
- Verify, do not recall. Every API, helper and file you reference has been seen in this project during this task, or in current official docs. A plausible name is not a verified one. The kit's own contract is known: the shapes (
templates/core,BaseViewModel,launchGuarded,AppError) and every type or file the task context names count as seen. Never call an invented third-party method.NetworkException - Check the question before answering it. Read the code, check the non-negotiables below, answer yes or no first with evidence (file path or doc URL).
- Say no when the answer is no. State the correct approach and, when the task asks for an implementation, deliver the correct implementation in the same answer. A refusal without it is incomplete. Keep pushback short, plain-spoken and proportional (see the skill, Operating stance items 7–11). Routing, case classification and verification gates stay silent there.
compose-architecture - Unverifiable means say so. Say what you would need to check. Never present a guess as a fact.
- Fresh docs before new library code. Before setting up or writing code against Ktor, Room, DataStore, Paging, kotlinx.serialization or any new SDK: read the version in , read the current official docs for that version, then write. Unreachable docs means marking the code unverified. The full contract lives in the
gradle/libs.versions.tomlskill.compose-architecture
- 验证,不要凭记忆。 你在本次任务中提到的每一个 API、helper 和 file,都必须已在此项目中看到,或者已见于当前官方文档。看似合理的名称不等于经过验证的名称。该 kit 自身的 contract 是已知的:中的 shape(
templates/core、BaseViewModel、launchGuarded、AppError),以及 task context 中点名的每一个 type 或 file,均视为已见。绝不能调用杜撰出来的第三方 method。NetworkException - 回答前先检查问题。 阅读代码,检查下面的不可协商规则,并先用证据(file path 或 doc URL)给出 yes 或 no。
- 答案为 no 时就直说。 说明正确做法;如果任务要求 implementation,就在同一答复中给出正确 implementation。只拒绝而不给出做法是不完整的。提出异议应简短、直白且与问题相称(参见 skill,Operating stance 第 7–11 项)。路由、case classification 和 verification gate 在此保持静默。
compose-architecture - 无法验证时必须说明。 讲清楚还需要检查什么。绝不能把猜测当作事实。
- 编写新 library 代码前先查阅最新文档。 在配置或基于 Ktor、Room、DataStore、Paging、kotlinx.serialization 或任何新 SDK 编写代码前,先读取 中的版本,再阅读该版本的 当前官方文档,然后才能编写。文档无法访问时,必须将代码标记为未验证。完整 contract 位于
gradle/libs.versions.tomlskill。compose-architecture
When NOT to use
何时不要使用
| Task | Use instead |
|---|---|
| Route first: decide the task path and files to read | the |
ViewModel, Contract, Route, error tier choice, | the |
| Composables, lists, stability, resources | the |
| the |
| Modules, convention plugins, version catalog, CI | the |
| 任务 | 改用 |
|---|---|
| 首先进行路由:先决定任务路径和需要读取的文件 | |
ViewModel、Contract、Route、error tier 选择、 | |
| Composable、列表、稳定性、资源 | |
| |
| Module、convention plugin、version catalog、CI | |
Non-negotiables
不可协商规则
Rules 1–10 are non-negotiables. Rule 11 is a default: a recorded project decision in wins with no argument; waiving a non-negotiable needs a recorded reason (see the skill, 5).
## Project decisionscompose-architectureexisting-projects.md- DTOs and entities stay to the data layer. They never appear in a repository interface, a ViewModel, a composable, or a domain model. The mapper is required even when the fields are identical today. Prevents: a wire rename becoming a UI change.
internal - Domain models carry , never wire strings, and no serialization annotations. No ISO strings, no epoch millis, no
kotlin.time.Instant, no API field names, no Compose. Prevents: hot-path parsing on every bind.@Serializable - DTO-to-domain mapping is mandatory; domain-to-UiModel mapping follows the M-11 rule. Every aggregate maps DTO to domain with one in
toDomain(); the mapper performs no I/O and holds no shared state. Domain to UiModel happens only when an M-11 trigger fires — see thedata/remote/mapper/skill ("UiModel triggers (M-11)"), which owns the triggers; this skill does not restate them. Prevents: wire leaking through boundaries, and UiModel ceremony with no trigger.compose-architecture - A missing field never becomes a valid business value. Preserve absence () or drop the record. Never substitute "now", zero, an empty string, or an empty-but-valid default. Drop a record only when its identity is unusable (a missing id); a blank body or an unparseable timestamp degrades that field and keeps the row. Prevents: silent loss of a record the user needed.
null - Transport failures propagate to . No
launchGuardedin a repository or data source swallows them. A remote data source may catchcatchonly to map 404 toClientRequestExceptionon a by-identity read and must rethrow every other status (seenull). A repository never catchesnetworking-ktor.mdto keep a stale list silently. The ViewModel'sNetworkExceptiondecides the tier. Prevents: stale data with no message and no retry.onError - is a separate
PagingData, never aFlowfield. Copying state re-emits the list and it jumps to the top. The paging path never entersUiState;launchGuardedis handled at the UI boundary. Prevents: scroll-reset and swallowed page failures.LoadState - A detail destination fetches by identity from the key. The rule lives in the skill (rules 10, 15); a key that resolves only to
compose-architectureis broken on restore. Prevents: destinations that are broken on restore.null - on the Ktor client. Non-2xx responses throw (
expectSuccess = true,ClientRequestException) and the classifier maps them toServerResponseException. Manual per-call status inspection is error-tossing. Prevents: per-call status checks that hide the error path.NetworkException.Http - Shared-code imports live in the skill (rule 11). Say the platform next to every data API that is Android-only. Prevents: shared code that compiles on one target only.
compose-ui - One DataStore instance per file, injected as a Koin . Preferences DataStore in
single; structured values ride as one JSON string key. Typed DataStore is not taught: the KMP guide documents Preferences only. Never point Desktop storage at the shared temp directory. Prevents: store corruption and Android-only storage in shared code.commonMain - Repository reads name their async contract (default). for one-shots,
suspend fun getX(…)for streams; neverfun getXStream(…): Flow<…>,observeX,getXFlow, never one name for both. Owned by thegetXPagerskill (rule 12); the detail lives there. Prevents: async-contract confusion.compose-architecture
Version gates: if shows Room below the first KMP-stable release, Paging below the first release with support, or kotlinx.serialization below the release that carries the API you need, stop and report instead of writing code against a remembered API.
gradle/libs.versions.tomlcommonMain规则 1–10 是 不可协商规则。规则 11 是默认规则: 中记录的项目决策无需争辩即可优先采用。要豁免某条不可协商规则,必须记录原因(参见 skill, 第 5 项)。
## Project decisionscompose-architectureexisting-projects.md- DTO 和 entity 必须始终留在 data layer 内部并保持 。 它们绝不能出现在 repository interface、ViewModel、composable 或 domain model 中。即使当前字段完全相同,mapper 仍是必需的。防止: wire rename 演变成 UI change。
internal - domain model 必须使用 ,绝不能使用 wire string,也不得包含 serialization annotation。 不得使用 ISO string、epoch millis、
kotlin.time.Instant、API field name 或 Compose。防止: 在每次 bind 时都进行 hot-path parsing。@Serializable - DTO 到 domain 的 mapping 是强制要求;domain 到 UiModel 的 mapping 则遵循 M-11 规则。 每个 aggregate 都必须在 中通过一个
data/remote/mapper/完成 DTO 到 domain 的 mapping;mapper 不执行 I/O,也不持有 shared state。只有在 M-11 trigger 触发时,才进行 domain 到 UiModel mapping——参见toDomain()skill(“UiModel triggers (M-11)”),该 skill 负责定义这些 trigger;本 skill 不再重复。防止: wire 穿透各层边界,以及在没有 trigger 时徒增 UiModel 样板流程。compose-architecture - 缺失字段绝不能变成看似有效的业务值。 必须保留缺失状态(),或者丢弃该记录。绝不能替换为 “now”、0、空字符串或看似有效的空默认值。只有在记录 identity 不可用时(即 missing id)才能丢弃记录;blank body 或 unparseable timestamp 应降级处理对应 field,同时保留该 row。防止: 静默丢失用户需要的记录。
null - transport failure 必须传播到 。 repository 或 data source 中不得用
launchGuarded吞掉 failure。remote data source 仅可捕获catch,以便在按 identity 读取时把 404 映射为ClientRequestException,并且必须重新抛出其他所有状态。repository 绝不能捕获null来静默保留 stale list。由 ViewModel 的NetworkException决定 tier。防止: 陈旧数据没有提示且无法重试。onError - 必须是独立的
PagingData,绝不能成为Flowfield。 复制 state 会再次 emit 列表,导致列表跳回顶部。paging path 绝不进入UiState;launchGuarded在 UI boundary 处理。防止: 滚动位置重置和页面失败被吞掉。LoadState - detail destination 必须根据 key 按 identity fetch。 该规则位于 skill(规则 10、15);只能解析为
compose-architecture的 key 在 restore 后会失效。防止: detail destination 在 restore 后无法正常使用。null - Ktor client 必须设置 。 非 2xx response 会抛出(
expectSuccess = true、ClientRequestException),classifier 将其映射为ServerResponseException。逐 call 手动检查 status 是在逃避错误处理。防止: 逐 call 检查 status,从而掩盖错误路径。NetworkException.Http - shared code 的 import 规则位于 skill(规则 11)。 对于每个 Android-only data API,必须同时标明 platform。防止: shared code 只能在单一 target 上编译。
compose-ui - 每个 file 只能有一个 DataStore instance,并通过 Koin 注入。 Preferences DataStore 放在
single;structured value 以单个 JSON string key 承载。本文不涉及 Typed DataStore:KMP guide 仅记录了 Preferences。绝不能让 Desktop storage 指向 shared temp directory。防止: store corruption,以及 shared code 中出现 Android-only storage。commonMain - repository read 必须明确命名其 async contract(默认约定)。 一次性读取使用 ,stream 使用
suspend fun getX(…);绝不使用fun getXStream(…): Flow<…>、observeX、getXFlow,也绝不能让同一名称同时表示两者。该规范由getXPagerskill(规则 12)负责;具体细节位于该 skill。防止: async contract 产生混淆。compose-architecture
Version gate:如果 显示 Room 低于首个 KMP-stable 版本、Paging 低于首个支持 的版本,或者 kotlinx.serialization 低于包含所需 API 的版本,应停止并报告,而不是根据记忆中的 API 编写代码。
gradle/libs.versions.tomlcommonMainWorkflow
工作流
- Create one todo per step below and do them in order.
- Name the repository contract first: one-shots (), streams (
getX), writes (verb phrases). The interface lives ingetXStreamand exposes domain types only.domain/repository/ - Decide layers and mappers before any client code: DTO shape, placement, whether an M-11 trigger fires (name it or skip the UiModel).
toDomain() - Read the project's versions in and the current official docs for every library touched, then write.
gradle/libs.versions.toml - Enumerate failure paths: which tier each gets (tiers live in the skill), what retry holds, what stays silent only as a named poll.
compose-architecture - Enumerate lifecycle cases: cold load vs reconcile for one-shots (streams reconcile by re-emission), overlapping loads, process-death restore of a detail destination.
- Run the Verification gates below.
- 为下列每一步创建一个 ,并按顺序执行。
todo - 首先明确 repository contract:一次性读取()、stream(
getX)、write(动词短语)。interface 位于getXStream,并且只暴露 domain type。domain/repository/ - 在编写任何 client code 前确定 layer 和 mapper:DTO shape、的位置,以及是否触发 M-11(指出 trigger,或跳过 UiModel)。
toDomain() - 读取项目的 ,并查阅涉及的每个 library 的当前官方文档,然后才能编写代码。
gradle/libs.versions.toml - 枚举 failure path:每个 failure 属于哪个 tier(tier 定义在 skill 中)、哪些应保留 retry、哪些仅能作为已命名 poll 保持静默。
compose-architecture - 枚举 lifecycle case:一次性读取的 cold load 与 reconcile(stream 通过 re-emission 进行 reconcile)、重叠 load,以及 detail destination 在 process death 后的 restore。
- 执行下面的 Verification gate。
Decision tables
决策表
Local storage choice
本地存储选择
| Need | Store |
|---|---|
| Key-value settings, flags, tokens | Preferences DataStore (rule 10) |
| Structured settings object | One JSON string key in Preferences DataStore (rule 10) |
| Queries, indexes, relations, more than ~100 entries | Room |
| Large blobs (images, files) | Filesystem, with only the path in Room or DataStore |
| 需求 | 存储方式 |
|---|---|
| 键值设置、标志、token | Preferences DataStore(规则 10) |
| 结构化设置对象 | Preferences DataStore 中的单个 JSON string key(规则 10) |
| 查询、索引、关系,超过约 100 条记录 | Room |
| 大型 blob(图片、文件) | 文件系统,仅在 Room 或 DataStore 中保存路径 |
Realtime feed choice
实时 feed 选择
| Need | Transport |
|---|---|
| Server-push text feed over HTTP, auto-reconnect wanted | SSE |
| Bidirectional, binary, or realtime collaboration | WebSocket |
A paged list over a changing network source uses plus Room; the UI observes the Room-backed source. returns only when the cache is fresh, else .
RemoteMediatorRemoteMediator.initializeSKIP_INITIAL_REFRESHLAUNCH_INITIAL_REFRESH| 需求 | 传输方式 |
|---|---|
| 通过 HTTP 推送服务端文本 feed,且需要自动重连 | SSE |
| 双向、二进制或实时协作 | WebSocket |
对于基于持续变化的网络源的 paged list,使用 加 Room;UI 观察由 Room 提供支持的数据源。仅当 cache 仍新鲜时, 才返回 ,否则返回 。
RemoteMediatorRemoteMediator.initializeSKIP_INITIAL_REFRESHLAUNCH_INITIAL_REFRESHRed flags
危险信号
| Thought | Reality |
|---|---|
| "I'll import the DTO into the ViewModel; the fields are identical." | No. Rule 1: DTOs stay |
| "I'll keep the timestamp as a string in the domain; parsing is cheap." | No. Rule 2: domain carries |
| "I'll default the missing reminder to now so the field is never null." | No. Rule 4: absence stays |
"I'll catch | No. Rule 5: failures propagate to |
"I'll map the timeout to | No. The |
"I'll put | No. Rule 6: a separate |
| "I'll resolve the detail note from the cached list; it is faster." | No. Rule 7: fetch by identity. The cache is cold after restore. |
| "I'll check the status code at each call site for control." | No. Rule 8: |
"I'll use | No. The |
"I'll add a | No. Rule 3 and the M-11 triggers in the |
| "This method probably exists on the client builder." | Stop and verify now (stance item 1). Inventing a third-party API does not compile. |
| 想法 | 实际情况 |
|---|---|
| “我会把 DTO import 到 ViewModel;字段完全相同。” | 不行。规则 1:DTO 必须保持 |
| “我会把 timestamp 作为 string 留在 domain;parsing 很便宜。” | 不行。规则 2:domain 必须使用 |
| “我会把缺失的 reminder 默认设为 now,这样 field 就永远不会为 null。” | 不行。规则 4:缺失状态必须保持 |
“我会在 repository 中捕获 | 不行。规则 5:failure 必须传播到 |
| “我会把 timeout 映射到 isMissing,这样就能显示 empty state。” | 不行。 |
“我会把 | 不行。规则 6:应使用独立的 |
| “我会从 cached list 中解析 detail note;这样速度更快。” | 不行。规则 7:必须按 identity fetch。restore 后 cache 是冷启动的。 |
| “我会在每个 call site 检查 status code 来控制行为。” | 不行。规则 8:使用 |
“我会在 | 不行。 |
“为了保持一致,我会添加 | 不行。规则 3 和 |
| “这个 method 很可能存在于 client builder 上。” | 立即停止并验证(工作立场第 1 项)。杜撰第三方 API 无法通过编译。 |
Verification
验证
- (or the skill's
scripts/composekit/run-checks.sh) exits 0.scripts/run-checks.sh <project-root> - Touched modules compile for common metadata and one platform; their JVM tests pass.
- No public or
*Dto:*Entityshows onlygrep -rn "public .*Dto\|^class .*Dto\|^data class .*Dto" --include='*.kt' <data-root>declarations.internal - No wire strings in domain: returns nothing.
grep -rn "String.*[Aa]t\b\|.*Date.*String\|@SerialName\|@Serializable" --include='*.kt' <domain-model-root> - No swallowed transport failure: returns nothing.
grep -rn "catch.*NetworkException" --include='*.kt' <data-root> - Every -returning repository read ends in
Flow(see theStreamskill, rule 12); nocompose-architecture,observeX,getXFlow: yes or no.getXPager - Every data file is free of
commonMain: yes or no.^import (java|android)\. - Every library API named in the change was seen in the current official docs for the version in : yes or no.
libs.versions.toml - Every UiModel added names its M-11 trigger in a one-line comment: yes or no.
- (或该 skill 的
scripts/composekit/run-checks.sh)以状态码 0 退出。scripts/run-checks.sh <project-root> - 所修改的 module 能够针对 common metadata 和一个 platform 完成编译;其 JVM test 全部通过。
- 不存在 public 或
*Dto:*Entity的结果仅包含grep -rn "public .*Dto\|^class .*Dto\|^data class .*Dto" --include='*.kt' <data-root>声明。internal - domain 中不存在 wire string:不返回任何结果。
grep -rn "String.*[Aa]t\b\|.*Date.*String\|@SerialName\|@Serializable" --include='*.kt' <domain-model-root> - 不存在被吞掉的 transport failure:不返回任何结果。
grep -rn "catch.*NetworkException" --include='*.kt' <data-root> - 每个返回 的 repository read 都以
Flow结尾(参见Streamskill,规则 12);不存在compose-architecture、observeX、getXFlow:yes 或 no。getXPager - 每个 data file 都不包含
commonMain:yes 或 no。^import (java|android)\. - 变更中提到的每个 library API,都已见于 所指定版本的当前官方文档:yes 或 no。
libs.versions.toml - 每个新增的 UiModel 都通过一行注释注明其 M-11 trigger:yes 或 no。
Reference lookup
Reference 查询
Load only the references this task needs. One level deep.
-
Also read coroutines-flow.md only when changing file or database writes and streams.
-
Also read error-handling.md only when changing failed writes or network calls.
-
boundaries-and-mapping.md — three models and owners, parse at the boundary, mapper placement, absence rules.
-
networking-ktor.md — client rules and gotchas:, engines, plugins, timeouts, classification, cancellation.
expectSuccess -
auth-and-realtime.md — bearer refresh, refresh-client split, WebSocket vs SSE, reconnect.
-
room.md — KMP setup, DAOs, transactions, relations, indexes, migrations.
-
datastore.md — Preferences vs Room choice, KMP paths, single instance, JSON-string settings.
-
paging.md —outside state,
PagingDataplacement, filters,cachedIn, MVI hookup.LoadState -
offline-first.md —plus Room,
RemoteMediatorchoice, cache timeout.InitializeAction -
data-testing.md —with the production factory, fakes over mocks, dispatcher control.
MockEngine
仅加载本任务需要的 reference,且仅深入一层。
-
仅在修改 file 或 database write 与 stream 时,同时阅读 coroutines-flow.md。
-
仅在修改失败写入或网络调用时,同时阅读 error-handling.md。
-
boundaries-and-mapping.md——三个 model 及其 owner、在 boundary 解析、mapper placement、缺失值规则。
-
networking-ktor.md——client 规则与常见陷阱:、engine、plugin、timeout、classification、cancellation。
expectSuccess -
auth-and-realtime.md——bearer refresh、refresh client 拆分、WebSocket 与 SSE、reconnect。
-
room.md——KMP setup、DAO、transaction、relation、index、migration。
-
datastore.md——Preferences 与 Room 的选择、KMP path、单实例、JSON string 设置。
-
paging.md——state 外的、
PagingDataplacement、filter、cachedIn、MVI hookup。LoadState -
offline-first.md——加 Room、
RemoteMediator选择、cache timeout。InitializeAction -
data-testing.md——使用 production factory 的、优先使用 fake 而非 mock、dispatcher 控制。
MockEngine