compose-data
Skill303 starsChanged 43 days agoMITView on GitHub
Owns repositories, data sources and mapping for Compose and Compose Multiplatform apps: DTO to domain to UiModel boundaries, Ktor clients and bearer auth, WebSocket and SSE, Room, DataStore, Paging 3, offline-first and data-layer tests. Use when writing or reviewing a repository, data source, DTO, mapper, HttpClient, bearer token, WebSocket, SSE, Room DAO, migration, DataStore, PagingSource, RemoteMediator, offline cache or MockEngine test. Do NOT use for ViewModel or UI wiring (compose-feature), composables (compose-ui), the MVI contract or error tiers (compose-architecture), Gradle modules (compose-project), or commonMain vs platform splits (compose-platform).
In this repository7 other files it gives its agents.
- .cursor/rules/reference-file-standards.mdcCursor rule
- compose-architectureSkill
- compose-featureSkill
- compose-platformSkill
- compose-projectSkill
- composeSkill
- compose-uiSkill
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 20
Check the question before answering it: Read the code, check the non-negotiables below, answer yes or no first with evidence (file path or …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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.
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
templates/coreshapes (BaseViewModel,launchGuarded,AppError,NetworkException) and every type or file the task context names count as seen. Never call an invented third-party method. - 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
compose-architectureskill, Operating stance items 7–11). Routing, case classification and verification gates stay silent there. - 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
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 thecompose-architectureskill.
When NOT to use
| Task | Use instead |
| Route first: decide the task path and files to read | the 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, resources | the compose-ui skill |
commonMain vs platform splits, expect/actual, iOS/Swift export | the compose-platform skill |
| Modules, convention plugins, version catalog, CI | the 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).
- DTOs and entities stay
internalto 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. - 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. - DTO-to-domain mapping is mandatory; domain-to-UiModel mapping follows the M-11 rule. Every aggregate maps DTO to domain with one
toDomain()indata/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 thecompose-architectureskill ("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. - 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. - Transport failures propagate to
launchGuarded. Nocatchin a repository or data source swallows them. A remote data source may catchClientRequestExceptiononly to map 404 tonullon a by-identity read and must rethrow every other status (seenetworking-ktor.md). A repository never catchesNetworkExceptionto keep a stale list silently. The ViewModel'sonErrordecides the tier. Prevents: stale data with no message and no retry. PagingDatais a separateFlow, never aUiStatefield. Copying state re-emits the list and it jumps to the top. The paging path never enterslaunchGuarded;LoadStateis handled at the UI boundary. Prevents: scroll-reset and swallowed page failures.- A detail destination fetches by identity from the key. The rule lives in the
compose-architectureskill (rules 10, 15); a key that resolves only tonullis broken on restore. Prevents: destinations that are broken on restore. expectSuccess = trueon the Ktor client. Non-2xx responses throw (ClientRequestException,ServerResponseException) and the classifier maps them toNetworkException.Http. Manual per-call status inspection is error-tossing. Prevents: per-call status checks that hide the error path.- Shared-code imports live in the
compose-uiskill (rule 11). Say the platform next to every data API that is Android-only. Prevents: shared code that compiles on one target only. - One DataStore instance per file, injected as a Koin
single. Preferences DataStore incommonMain; 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. - Repository reads name their async contract (default).
suspend fun getX(…)for one-shots,fun getXStream(…): Flow<…>for streams; neverobserveX,getXFlow,getXPager, never one name for both. Owned by thecompose-architectureskill (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.
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 indomain/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.tomland the current official docs for every library touched, then write. - Enumerate failure paths: which tier each gets (tiers live in the
compose-architectureskill), 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.
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 |
Realtime feed choice
| 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 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.
Red flags
| Thought | Reality |
| "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. |
Verification
scripts/composekit/run-checks.sh(or the skill'sscripts/run-checks.sh <project-root>) exits 0.- Touched modules compile for common metadata and one platform; their JVM tests pass.
- No public
*Dtoor*Entity:grep -rn "public .*Dto\|^class .*Dto\|^data class .*Dto" --include='*.kt' <data-root>shows onlyinternaldeclarations. - 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 inStream(see thecompose-architectureskill, rule 12); noobserveX,getXFlow,getXPager: yes or no. - Every
commonMaindata 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.
Reference lookup
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 —
PagingDataoutside state,cachedInplacement, filters,LoadState, MVI hookup. - offline-first.md —
RemoteMediatorplus Room,InitializeActionchoice, cache timeout. - data-testing.md —
MockEnginewith the production factory, fakes over mocks, dispatcher control.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

