compose-data

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose 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

回答前验证契约

  1. 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
    templates/core
    shapes (
    BaseViewModel
    ,
    launchGuarded
    ,
    AppError
    ,
    NetworkException
    ) and every type or file the task context names count as seen. Never call an invented third-party method.
  2. 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).
  3. 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
    compose-architecture
    skill, Operating stance items 7–11). Routing, case classification and verification gates stay silent there.
  4. Unverifiable means say so. Say what you would need to check. Never present a guess as a fact.
  5. 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
    gradle/libs.versions.toml
    , read the current official docs for that version, then write. Unreachable docs means marking the code unverified. The full contract lives in the
    compose-architecture
    skill.
  1. 验证,不要凭记忆。 你在本次任务中提到的每一个 API、helper 和 file,都必须已在此项目中看到,或者已见于当前官方文档。看似合理的名称不等于经过验证的名称。该 kit 自身的 contract 是已知的:
    templates/core
    中的 shape(
    BaseViewModel
    、
    launchGuarded
    、
    AppError
    、
    NetworkException
    ),以及 task context 中点名的每一个 type 或 file,均视为已见。绝不能调用杜撰出来的第三方 method。
  2. 回答前先检查问题。 阅读代码,检查下面的不可协商规则,并先用证据(file path 或 doc URL)给出 yes 或 no。
  3. 答案为 no 时就直说。 说明正确做法;如果任务要求 implementation,就在同一答复中给出正确 implementation。只拒绝而不给出做法是不完整的。提出异议应简短、直白且与问题相称(参见
    compose-architecture
    skill,Operating stance 第 7–11 项)。路由、case classification 和 verification gate 在此保持静默。
  4. 无法验证时必须说明。 讲清楚还需要检查什么。绝不能把猜测当作事实。
  5. 编写新 library 代码前先查阅最新文档。 在配置或基于 Ktor、Room、DataStore、Paging、kotlinx.serialization 或任何新 SDK 编写代码前,先读取
    gradle/libs.versions.toml
    中的版本,再阅读该版本的 当前官方文档,然后才能编写。文档无法访问时,必须将代码标记为未验证。完整 contract 位于
    compose-architecture
    skill。

When NOT to use

何时不要使用

TaskUse instead
Route first: decide the task path and files to readthe
compose
skill, before anything below
ViewModel, Contract, Route, error tier choice,
launchGuarded
wiring
the
compose-architecture
skill (tiers) and the
compose-feature
skill (workflow)
Composables, lists, stability, resourcesthe
compose-ui
skill
commonMain
vs platform splits,
expect
/
actual
, iOS/Swift export
the
compose-platform
skill
Modules, convention plugins, version catalog, CIthe
compose-project
skill
任务改用
首先进行路由:先决定任务路径和需要读取的文件
compose
skill,并且先于以下所有内容
ViewModel、Contract、Route、error tier 选择、
launchGuarded
wiring
compose-architecture
skill(tier)和
compose-feature
skill(workflow)
Composable、列表、稳定性、资源
compose-ui
skill
commonMain
与 platform 的拆分、
expect
/
actual
、iOS/Swift export
compose-platform
skill
Module、convention plugin、version catalog、CI
compose-project
skill

Non-negotiables

不可协商规则

Rules 1–10 are non-negotiables. Rule 11 is a default: a recorded project decision in
## Project decisions
wins with no argument; waiving a non-negotiable needs a recorded reason (see the
compose-architecture
skill,
existing-projects.md
5).
  1. DTOs and entities stay
    internal
    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.
  2. Domain models carry
    kotlin.time.Instant
    , never wire strings, and no serialization annotations.
    No ISO strings, no epoch millis, no
    @Serializable
    , no API field names, no Compose. Prevents: hot-path parsing on every bind.
  3. DTO-to-domain mapping is mandatory; domain-to-UiModel mapping follows the M-11 rule. Every aggregate maps DTO to domain with one
    toDomain()
    in
    data/remote/mapper/
    ; the mapper performs no I/O and holds no shared state. Domain to UiModel happens only when an M-11 trigger fires — see the
    compose-architecture
    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.
  4. A missing field never becomes a valid business value. Preserve absence (
    null
    ) 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.
  5. Transport failures propagate to
    launchGuarded
    . No
    catch
    in a repository or data source swallows them.
    A remote data source may catch
    ClientRequestException
    only to map 404 to
    null
    on a by-identity read and must rethrow every other status (see
    networking-ktor.md
    ). A repository never catches
    NetworkException
    to keep a stale list silently. The ViewModel's
    onError
    decides the tier. Prevents: stale data with no message and no retry.
  6. PagingData
    is a separate
    Flow
    , never a
    UiState
    field.
    Copying state re-emits the list and it jumps to the top. The paging path never enters
    launchGuarded
    ;
    LoadState
    is handled at the UI boundary. Prevents: scroll-reset and swallowed page failures.
  7. A detail destination fetches by identity from the key. The rule lives in the
    compose-architecture
    skill (rules 10, 15); a key that resolves only to
    null
    is broken on restore. Prevents: destinations that are broken on restore.
  8. expectSuccess = true
    on the Ktor client.
    Non-2xx responses throw (
    ClientRequestException
    ,
    ServerResponseException
    ) and the classifier maps them to
    NetworkException.Http
    . Manual per-call status inspection is error-tossing. Prevents: per-call status checks that hide the error path.
  9. Shared-code imports live in the
    compose-ui
    skill (rule 11).
    Say the platform next to every data API that is Android-only. Prevents: shared code that compiles on one target only.
  10. One DataStore instance per file, injected as a Koin
    single
    . Preferences DataStore in
    commonMain
    ; 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.
  11. Repository reads name their async contract (default).
    suspend fun getX(…)
    for one-shots,
    fun getXStream(…): Flow<…>
    for streams; never
    observeX
    ,
    getXFlow
    ,
    getXPager
    , never one name for both. Owned by the
    compose-architecture
    skill (rule 12); the detail lives there. Prevents: async-contract confusion.
Version gates: if
gradle/libs.versions.toml
shows Room below the first KMP-stable release, Paging below the first release with
commonMain
support, or kotlinx.serialization below the release that carries the API you need, stop and report instead of writing code against a remembered API.
规则 1–10 是 不可协商规则。规则 11 是默认规则:
## Project decisions
中记录的项目决策无需争辩即可优先采用。要豁免某条不可协商规则,必须记录原因(参见
compose-architecture
skill,
existing-projects.md
第 5 项)。
  1. DTO 和 entity 必须始终留在 data layer 内部并保持
    internal
    。
    它们绝不能出现在 repository interface、ViewModel、composable 或 domain model 中。即使当前字段完全相同,mapper 仍是必需的。防止: wire rename 演变成 UI change。
  2. domain model 必须使用
    kotlin.time.Instant
    ,绝不能使用 wire string,也不得包含 serialization annotation。
    不得使用 ISO string、epoch millis、
    @Serializable
    、API field name 或 Compose。防止: 在每次 bind 时都进行 hot-path parsing。
  3. DTO 到 domain 的 mapping 是强制要求;domain 到 UiModel 的 mapping 则遵循 M-11 规则。 每个 aggregate 都必须在
    data/remote/mapper/
    中通过一个
    toDomain()
    完成 DTO 到 domain 的 mapping;mapper 不执行 I/O,也不持有 shared state。只有在 M-11 trigger 触发时,才进行 domain 到 UiModel mapping——参见
    compose-architecture
    skill(“UiModel triggers (M-11)”),该 skill 负责定义这些 trigger;本 skill 不再重复。防止: wire 穿透各层边界,以及在没有 trigger 时徒增 UiModel 样板流程。
  4. 缺失字段绝不能变成看似有效的业务值。 必须保留缺失状态(
    null
    ),或者丢弃该记录。绝不能替换为 “now”、0、空字符串或看似有效的空默认值。只有在记录 identity 不可用时(即 missing id)才能丢弃记录;blank body 或 unparseable timestamp 应降级处理对应 field,同时保留该 row。防止: 静默丢失用户需要的记录。
  5. transport failure 必须传播到
    launchGuarded
    。
    repository 或 data source 中不得用
    catch
    吞掉 failure。remote data source 仅可捕获
    ClientRequestException
    ,以便在按 identity 读取时把 404 映射为
    null
    ,并且必须重新抛出其他所有状态。repository 绝不能捕获
    NetworkException
    来静默保留 stale list。由 ViewModel 的
    onError
    决定 tier。防止: 陈旧数据没有提示且无法重试。
  6. PagingData
    必须是独立的
    Flow
    ,绝不能成为
    UiState
    field。
    复制 state 会再次 emit 列表,导致列表跳回顶部。paging path 绝不进入
    launchGuarded
    ;
    LoadState
    在 UI boundary 处理。防止: 滚动位置重置和页面失败被吞掉。
  7. detail destination 必须根据 key 按 identity fetch。 该规则位于
    compose-architecture
    skill(规则 10、15);只能解析为
    null
    的 key 在 restore 后会失效。防止: detail destination 在 restore 后无法正常使用。
  8. Ktor client 必须设置
    expectSuccess = true
    。
    非 2xx response 会抛出(
    ClientRequestException
    、
    ServerResponseException
    ),classifier 将其映射为
    NetworkException.Http
    。逐 call 手动检查 status 是在逃避错误处理。防止: 逐 call 检查 status,从而掩盖错误路径。
  9. shared code 的 import 规则位于
    compose-ui
    skill(规则 11)。
    对于每个 Android-only data API,必须同时标明 platform。防止: shared code 只能在单一 target 上编译。
  10. 每个 file 只能有一个 DataStore instance,并通过 Koin
    single
    注入。
    Preferences DataStore 放在
    commonMain
    ;structured value 以单个 JSON string key 承载。本文不涉及 Typed DataStore:KMP guide 仅记录了 Preferences。绝不能让 Desktop storage 指向 shared temp directory。防止: store corruption,以及 shared code 中出现 Android-only storage。
  11. repository read 必须明确命名其 async contract(默认约定)。 一次性读取使用
    suspend fun getX(…)
    ,stream 使用
    fun getXStream(…): Flow<…>
    ;绝不使用
    observeX
    、
    getXFlow
    、
    getXPager
    ,也绝不能让同一名称同时表示两者。该规范由
    compose-architecture
    skill(规则 12)负责;具体细节位于该 skill。防止: async contract 产生混淆。
Version gate:如果
gradle/libs.versions.toml
显示 Room 低于首个 KMP-stable 版本、Paging 低于首个支持
commonMain
的版本,或者 kotlinx.serialization 低于包含所需 API 的版本,应停止并报告,而不是根据记忆中的 API 编写代码。

Workflow

工作流

  • Create one todo per step below and do them in order.
  • Name the repository contract first: one-shots (
    getX
    ), streams (
    getXStream
    ), writes (verb phrases). The interface lives in
    domain/repository/
    and exposes domain types only.
  • Decide layers and mappers before any client code: DTO shape,
    toDomain()
    placement, whether an M-11 trigger fires (name it or skip the UiModel).
  • Read the project's versions in
    gradle/libs.versions.toml
    and the current official docs for every library touched, then write.
  • Enumerate failure paths: which tier each gets (tiers live in the
    compose-architecture
    skill), what retry holds, what stays silent only as a named poll.
  • 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:一次性读取(
    getX
    )、stream(
    getXStream
    )、write(动词短语)。interface 位于
    domain/repository/
    ,并且只暴露 domain type。
  • 在编写任何 client code 前确定 layer 和 mapper:DTO shape、
    toDomain()
    的位置,以及是否触发 M-11(指出 trigger,或跳过 UiModel)。
  • 读取项目的
    gradle/libs.versions.toml
    ,并查阅涉及的每个 library 的当前官方文档,然后才能编写代码。
  • 枚举 failure path:每个 failure 属于哪个 tier(tier 定义在
    compose-architecture
    skill 中)、哪些应保留 retry、哪些仅能作为已命名 poll 保持静默。
  • 枚举 lifecycle case:一次性读取的 cold load 与 reconcile(stream 通过 re-emission 进行 reconcile)、重叠 load,以及 detail destination 在 process death 后的 restore。
  • 执行下面的 Verification gate。

Decision tables

决策表

Local storage choice

本地存储选择

NeedStore
Key-value settings, flags, tokensPreferences DataStore (rule 10)
Structured settings objectOne JSON string key in Preferences DataStore (rule 10)
Queries, indexes, relations, more than ~100 entriesRoom
Large blobs (images, files)Filesystem, with only the path in Room or DataStore
需求存储方式
键值设置、标志、tokenPreferences DataStore(规则 10)
结构化设置对象Preferences DataStore 中的单个 JSON string key(规则 10)
查询、索引、关系,超过约 100 条记录Room
大型 blob(图片、文件)文件系统,仅在 Room 或 DataStore 中保存路径

Realtime feed choice

实时 feed 选择

NeedTransport
Server-push text feed over HTTP, auto-reconnect wantedSSE
Bidirectional, binary, or realtime collaborationWebSocket
A paged list over a changing network source uses
RemoteMediator
plus Room; the UI observes the Room-backed source.
RemoteMediator.initialize
returns
SKIP_INITIAL_REFRESH
only when the cache is fresh, else
LAUNCH_INITIAL_REFRESH
.
需求传输方式
通过 HTTP 推送服务端文本 feed,且需要自动重连SSE
双向、二进制或实时协作WebSocket
对于基于持续变化的网络源的 paged list,使用
RemoteMediator
加 Room;UI 观察由 Room 提供支持的数据源。仅当 cache 仍新鲜时,
RemoteMediator.initialize
才返回
SKIP_INITIAL_REFRESH
,否则返回
LAUNCH_INITIAL_REFRESH
。

Red flags

危险信号

ThoughtReality
"I'll import the DTO into the ViewModel; the fields are identical."No. Rule 1: DTOs stay
internal
. The mapper is required even at 1:1.
"I'll keep the timestamp as a string in the domain; parsing is cheap."No. Rule 2: domain carries
Instant
. Every card re-parses it on every bind.
"I'll default the missing reminder to now so the field is never null."No. Rule 4: absence stays
null
. "Now" is a fabricated business value.
"I'll catch
NetworkException
in the repository and keep the stale list."
No. Rule 5: failures propagate to
launchGuarded
. Stale with no retry is silent data loss.
"I'll map the timeout to
isMissing
so the empty state shows."
No. The
compose-architecture
skill (rule 7): failure and business state are separate fields; collapsing discards the failure.
"I'll put
PagingData
in
UiState
so everything is in one place."
No. Rule 6: a separate
Flow
. State copies re-emit and the list jumps.
"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:
expectSuccess = true
. Per-call inspection is error-tossing.
"I'll use
java.time
in
commonMain
; it is the same API."
No. The
compose-ui
skill (rule 11):
kotlin.time.Instant
.
java.*
never appears in shared code.
"I'll add a
NoteUiModel
plus mapper for consistency."
No. Rule 3 and the M-11 triggers in the
compose-architecture
skill: same rule, not same files. Name the trigger or skip the pair.
"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 必须保持
internal
。即使比例为 1:1,也必须使用 mapper。
“我会把 timestamp 作为 string 留在 domain;parsing 很便宜。”不行。规则 2:domain 必须使用
Instant
。否则每张卡片在每次 bind 时都要重新解析。
“我会把缺失的 reminder 默认设为 now,这样 field 就永远不会为 null。”不行。规则 4:缺失状态必须保持
null
。“Now” 是伪造的业务值。
“我会在 repository 中捕获
NetworkException
,然后保留 stale list。”
不行。规则 5:failure 必须传播到
launchGuarded
。没有重试的陈旧数据属于静默数据丢失。
“我会把 timeout 映射到 isMissing,这样就能显示 empty state。”不行。
compose-architecture
skill(规则 7):failure 与 business state 是两个独立字段;将二者合并会丢弃 failure。
“我会把
PagingData
放进
UiState
,这样所有内容都在一个地方。”
不行。规则 6:应使用独立的
Flow
。复制 state 会再次 emit,导致列表跳动。
“我会从 cached list 中解析 detail note;这样速度更快。”不行。规则 7:必须按 identity fetch。restore 后 cache 是冷启动的。
“我会在每个 call site 检查 status code 来控制行为。”不行。规则 8:使用
expectSuccess = true
。逐 call 检查是在逃避错误处理。
“我会在
commonMain
中使用
java.time
;API 是一样的。”
不行。
compose-ui
skill(规则 11):使用
kotlin.time.Instant
。shared code 中绝不出现
java.*
。
“为了保持一致,我会添加
NoteUiModel
和 mapper。”
不行。规则 3 和
compose-architecture
skill 中的 M-11 trigger:规则相同不代表文件也应相同。指出 trigger,或者省略这对文件。
“这个 method 很可能存在于 client builder 上。”立即停止并验证(工作立场第 1 项)。杜撰第三方 API 无法通过编译。

Verification

验证

  • scripts/composekit/run-checks.sh
    (or the skill's
    scripts/run-checks.sh <project-root>
    ) exits 0.
  • Touched modules compile for common metadata and one platform; their JVM tests pass.
  • No public
    *Dto
    or
    *Entity
    :
    grep -rn "public .*Dto\|^class .*Dto\|^data class .*Dto" --include='*.kt' <data-root>
    shows only
    internal
    declarations.
  • No wire strings in domain:
    grep -rn "String.*[Aa]t\b\|.*Date.*String\|@SerialName\|@Serializable" --include='*.kt' <domain-model-root>
    returns nothing.
  • No swallowed transport failure:
    grep -rn "catch.*NetworkException" --include='*.kt' <data-root>
    returns nothing.
  • Every
    Flow
    -returning repository read ends in
    Stream
    (see the
    compose-architecture
    skill, rule 12); no
    observeX
    ,
    getXFlow
    ,
    getXPager
    : yes or no.
  • Every
    commonMain
    data file is free of
    ^import (java|android)\.
    : yes or no.
  • Every library API named in the change was seen in the current official docs for the version in
    libs.versions.toml
    : yes or no.
  • Every UiModel added names its M-11 trigger in a one-line comment: yes or no.
  • scripts/composekit/run-checks.sh
    (或该 skill 的
    scripts/run-checks.sh <project-root>
    )以状态码 0 退出。
  • 所修改的 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>
    不返回任何结果。
  • 每个返回
    Flow
    的 repository read 都以
    Stream
    结尾(参见
    compose-architecture
    skill,规则 12);不存在
    observeX
    、
    getXFlow
    、
    getXPager
    :yes 或 no。
  • 每个
    commonMain
    data file 都不包含
    ^import (java|android)\.
    :yes 或 no。
  • 变更中提到的每个 library API,都已见于
    libs.versions.toml
    所指定版本的当前官方文档:yes 或 no。
  • 每个新增的 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:
    expectSuccess
    , engines, plugins, timeouts, classification, cancellation.
  • 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 —
    PagingData
    outside state,
    cachedIn
    placement, filters,
    LoadState
    , MVI hookup.
  • offline-first.md —
    RemoteMediator
    plus Room,
    InitializeAction
    choice, cache timeout.
  • data-testing.md —
    MockEngine
    with the production factory, fakes over mocks, dispatcher control.
仅加载本任务需要的 reference,且仅深入一层。
  • 仅在修改 file 或 database write 与 stream 时,同时阅读 coroutines-flow.md。
  • 仅在修改失败写入或网络调用时,同时阅读 error-handling.md。
  • boundaries-and-mapping.md——三个 model 及其 owner、在 boundary 解析、mapper placement、缺失值规则。
  • networking-ktor.md——client 规则与常见陷阱:
    expectSuccess
    、engine、plugin、timeout、classification、cancellation。
  • 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 外的
    PagingData
    、
    cachedIn
    placement、filter、
    LoadState
    、MVI hookup。
  • offline-first.md——
    RemoteMediator
    加 Room、
    InitializeAction
    选择、cache timeout。
  • data-testing.md——使用 production factory 的
    MockEngine
    、优先使用 fake 而非 mock、dispatcher 控制。