compose-ui
Skill303 starsChanged 43 days agoMITView on GitHub
Writes and reviews composables for Compose and CMP: the Route/Screen/leaf split, state-read placement and stability, loading/empty/error UX states, LazyColumn lists and grids, animation choice, accessibility and semantics, design-system tokens, CMP Res resources, Coil images, and keyboard/focus. Use when touching @Composable code, recomposition, stability, LazyColumn, animation, shimmer or skeleton, accessibility, theme, colors, Res.string, Coil, focus, or keyboard. Do NOT use for MVI, error tiers, or module graph (compose-architecture), ViewModel or data work (compose-feature, compose-data), Gradle or module work (compose-project), or expect/actual splits (compose-platform).
5 other projects use a version of this.
Adopted by
Most recently taken up by
- JetBrains/intellij-communitytheir own version, first seen Nov 8, 2026
- IntelliHelper/agent-skillstheir own version, first seen Oct 20, 2026
- liuyi0808/android-reviewtheir own version, first seen Oct 13, 2026
- r0adkll/Campfiretheir own version, first seen Oct 9, 2026
- maxrave-dev/SimpMusictheir own version, first seen Oct 8, 2026
Other versions5 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 5
- JetBrains/intellij-community★ 21ksame name
- maxrave-dev/SimpMusic★ 13ksame name
- r0adkll/Campfire★ 151same name
- liuyi0808/android-review★ 17same name
- IntelliHelper/agent-skills★ 1same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository7 other files it gives its agents.
- .cursor/rules/reference-file-standards.mdcCursor rule
- compose-architectureSkill
- compose-dataSkill
- compose-featureSkill
- compose-platformSkill
- compose-projectSkill
- composeSkill
The file
We split this skill into the rules it gives an agent
3 rules. 0 are common, 2 are distinctive, and 1 is very specific to this project.
0
Common
Most of the 5 edited versions we read keep these.
2
Distinctive
This author's own choices. Worth a look.
1
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 19
Verify, do not recall: Every component, token, and helper you name was seen in this project during this task, or in current official docs.
- Distinctive: line 20
Check the question before answering it: Read the composable, check the non-negotiables, answer yes or no first with evidence.
- Very specific: line 22
Fresh docs before new library code: Read gradle/libs.versions.toml and the current official docs first.
“Common” is measured: kept by at least 6 in 10 of the 5 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Compose UI
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.
You reason about scope, not vibes. "Wrapping it in remember" and "adding @Immutable" are not diagnoses. The only question that matters is: when this value changes, which composable scopes re-execute? Answer that before changing a line. Satisfying the wording of a rule while defeating its purpose is a violation.
Validate-before-you-answer contract (condensed; full text in the compose-architecture skill)
- Verify, do not recall. Every component, token, and helper you name was seen in this project during this task, or in current official docs. Name an unverified need as an open gap, never call it.
- Check the question before answering it. Read the composable, check the non-negotiables, answer yes or no first with evidence.
- Say no when the answer is no. State the correct approach. If the user insists, restate the consequence once, follow the decision, record the deviation. 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. - Fresh docs before new library code. Read
gradle/libs.versions.tomland the current official docs first; unreachable docs means marking the code unverified.
When NOT to use
| Task | Use instead |
| Route first: decide the task path and files to read | the compose skill, before anything below |
| Add, change or review a screen, destination or slice | the compose-feature skill |
| Repositories, Ktor, Room, DataStore, Paging, offline-first | the compose-data skill |
commonMain sharing, expect/actual, iOS/Swift, desktop, web | the compose-platform skill |
| New project or module, convention plugins, version catalog, CI, guards | the compose-project skill |
| Navigation 3 API mechanics (scenes, decorators, deep-link recipes) | the android/skills navigation-3 skill, if installed; optional depth only |
Compiler-report internals beyond the loop in performance-diagnostics.md | the skydoves diagnosing-compose-stability skill, if installed; optional depth only |
Non-negotiables
Iron law: read depth decides cost. A state read invalidates the nearest enclosing composable scope, not the line that reads it. When a screen re-executes too often, move the read down to the leaf that renders it, never wrap the scope in
remember. A violation means deleting the hoisted read and re-verifying with the gates below.
Rules 1–3 and 5–12 are non-negotiables. Rule 4 is a default: a recorded project decision wins over it with no argument (see the compose-architecture skill, existing-projects.md item 5).
These rules govern code you write. When you review or conform existing working code, a deviation is a finding to report with its severity, not a reason to rewrite; follow the review and conform paths.
- Only the Route touches the ViewModel. The Screen is stateless (state in, callbacks out). Leaves take sub-state plus specific callbacks, never
onAction. A Screen that reads the ViewModel doubles every entry point; a leaf that takesonActioncan dispatch anything. Prevents: entry-point duplication and unreviewable leaves. - Read ticking or fast-changing state in the smallest scope that renders it. Never read it in a screen body, a
Scaffoldcontent lambda, or a lazy-list builder scope and pass the snapshot value down. When a deep consumer needs fast-changing state, pass theStateor a lambda, not the read value. A clock read above the list it feeds re-executes the box, the builder, and every visible item lambda on every tick. Prevents: per-tick list invalidation (brief F-15). - Clock- and animation-driven values never enter
UiStateas formatted strings.UiStateand UiModels carry theInstant; formatting happens in the presentation mapper or at display. A per-secondupdateStateof a remaining-time string invalidates every collector of that state. Prevents: unrestorable ticked state and whole-screen invalidation. - [Default] No UiModel without a named M-11 trigger; domain-type stability comes from the stability configuration file. Strong skipping is on by default: unstable parameters are compared by instance (
===), stable ones byequals. Types from a module built without the Compose compiler are never inferred stable. List only packages where every class is immutable; check the file when adding a model class. Also declarekotlin.collections.*(wired by build-logic).@Immutableis a promise the compiler believes without checking: apply it only when the class is genuinely immutable, or in-place mutation renders stale UI no recomposition fixes. Convert third-party unstable types at the boundary into a type the kit owns (LoadState.Errorholds aThrowable; presentAppErrorinstead). Never add a wrapper only for stability. Prevents: lost skipping and stale UI (brief F-16, F-17; M-11). - Colors come from theme tokens only; no hex literals in feature code. A hardcoded badge color misses every theme change. Prevents: theme breakage the guard catches.
- Reuse before writing: search the design-system module first. New shared components live in the design-system module, never in a feature. Copy a component together with the conditions at its call site. Prevents: reinvented components and precedent-gated UI breaking in its new home.
- Never clear or hide existing content during a refresh. Failure tiers live in the
compose-architectureskill (rule 8): skeletons for the cold load only, refresh keeps content with an indicator, a failed refresh keeps the items with a Retry holding it. Prevents: wiped content and trapped screens. - Every lazy list item has a stable key from domain identity plus a
contentType; never the index. Keep item scope light: hoist parsing and formatting out of item scope. Prevents: scrambled row state and per-tick parsing. - Every user-facing string is a resource, present in every locale. State holds semantic keys, never resolved strings; resolution happens at render. When reviewing existing code, a hardcoded string is a follow-up, not blocking, unless the task is localisation. Prevents: untranslated UI and locale drift the guard catches.
- Mount panes through the Navigation 3 scene strategy, give a pane's content the pane's width (never the window size class), and apply WindowInsets exactly once per screen. The scene strategy lets Back, deep link, and restore reach panes; a destination that can show in a pane picks its arrangement from the width it is given. Insets come from either the Scaffold inner padding or manual padding, never both, with
consumeWindowInsetschained after the applying padding. Prevents: unrestorable panes, wide arrangements in narrow panes, and double-offset content (AND-21, AND-23, AND-27, AND-72). - Code in
commonMainnever importsjava.*,android.*,LocalContext, orR; inject dispatchers and useDispatchers.Defaultas the shared default. Ajava.time.InstantorLocalContextreference compiles on Android and breaks every other target. Time iskotlin.time.Instant; strings are CMPResaccessors.Dispatchers.IOexists on Kotlin/Native since coroutines 1.7.0 but is unavailable when the module also targets JS/Wasm. Prevents: shared code that compiles on only some targets. - Adapt layouts to the window size class, never the device or platform, even when the request names
NotesTabletScreenorisTablet. One<Dest>Screenbranches once on a<Dest>Layoutvalue derived from the class the composition root computed; variants are named after the smallest class they serve (NotesExpandedLayout). A device check shows the wrong layout on an iPad in Split View, a folded phone, or a narrow desktop window. Prevents: device branches and*TabletScreencopies that drift apart.
Workflow
- Create one todo per step below and do them in order.
- Name the changing value and its frequency (per second, per keystroke, per scroll frame, per response). The frequency sets the budget.
- Name the smallest scope that must re-execute when it changes. Usually a single leaf.
- Find where the read happens today. A gap between the read and the scope is the defect.
- Check every parameter crossing into that scope for stability (rule 4 ladder below).
- Inventory the design-system module for components, formatters, and tokens before writing anything.
- Pick loading, empty, error, and validation visuals from the
ux-states.mddecision tables. - Run the Verification gates below.
Decision tables
Which stability fix (take the first rung that holds, then stop)
| Situation | Fix |
Domain model from a module without the Compose compiler sits in UiState | Declare its package in the stability configuration file (rule 4) |
Class is genuinely immutable (vals, read-only collections) and the report still flags it | @Immutable, describing truth only |
| An M-11 trigger fires (derived values, merged sources, UI-only fields, hidden fields) | UI-specific wrapper class |
| Anything else, including "make it skippable" on a mutable class | Stop. No annotation, no wrapper |
Red flags
| Thought | Reality |
"I'll read the clock at the top and pass now down; it is easier." | No. Rule 2: passing the value moves the cost up. Pass the State or read at the leaf. |
| "I'll tick a formatted countdown string from the ViewModel; simpler." | No. Rules 2–3: the string invalidates every collector each second. Carry the Instant; read the clock at the leaf. |
"I'll wrap it in remember to fix the recomposition." | No. Rule 2 (iron law): remember caches a value; it does not change which scope re-executes. Find the read. |
"Adding @Immutable will make it skippable." | No. Rule 4: only when the class is genuinely immutable, or mutation renders stale UI. |
| "Every feature needs a UiModel for consistency." | No. Rule 4: consistency means the same rule, not the same files. Name the M-11 trigger or hold the domain model. |
"I'll pass the whole NoteUiModel in the key so detail skips the fetch." | No. Keys carry identity; detail re-fetches by identity (see the compose-architecture skill, rule 15). |
| "This refresh can show a full-screen spinner; it is only a second." | No. Rule 7: refresh keeps content with an indicator. Skeletons are cold-load only. |
| "Index keys are fine; the list never reorders today." | No. Rule 8: keys come from domain identity, plus contentType. |
| "I'll hardcode this color; the tokens do not have it." | No. Rule 5: tokens only in feature code. Name the missing token as an open gap. |
| "I'll copy this component; the condition was specific to that screen." | No. Rule 6: copy the gate with the component. |
| "I'll put this string inline; translation comes later." | No. Rule 9: every user-facing string is a resource in every locale before done. |
"I'll use java.time / LocalContext here; it works on my device." | No. Rule 11: commonMain uses kotlin.time.Instant and CMP Res. Never java.*, android.*, LocalContext, or R. |
"I'll default this dispatcher to Dispatchers.IO; it is only shared code." | Rule 11: inject it and default to Dispatchers.Default in commonMain, which may also target JS/Wasm. |
"The user named NotesPhoneScreen and NotesTabletScreen, so I'll build those." | No. Rule 12: decline device names first; build one NotesScreen with size-class variants. |
"I'll keep isTablet; it is a width check now." | No. Rule 12: name the value after the layout (NotesLayout.Expanded), never the device. |
Verification
- Only the Route references the ViewModel; the Screen takes state plus callbacks (
rg -n "ViewModel|koinViewModel|collectAsState" --glob '*Screen.kt' --glob '*Sheet.kt'shows ViewModel reads only in*Route.kt). - No
rememberSaveablemirror of aUiStatefield and noLaunchedEffectsyncing two copies of one value (see thecompose-architectureskill, rules 9–10): yes or no. - No formatted countdown or clock string on
UiState; the clock is read at the leaf that renders it: yes or no. - The compiler stability report marks every owned
UiState/UiModelstable; every@Immutableholds onlyvals of immutable types (seeperformance-diagnostics.mdfor the loop). rg -n "Color\(0x" --glob '*.kt' <feature-root>is empty outside the design-system module (guardcheck-hardcoded-colors.sh).- Every
items(/item(call in a lazy list carries akeyfrom domain identity and acontentType: yes or no. - Strings exist in every locale folder with identical keys (guard
check-locale-parity.sh). rg -n "isTablet|isPhone|(Tablet|Phone|Mobile|Desktop|Web)(Screen|Layout)\b" --glob '*.kt' <feature-root>is empty.- Touched modules compile for common metadata and one platform; their JVM tests pass.
- The changed screen was run and looked at on one available target, or the answer says it was not run and why (see
run-and-see.md): yes or no.
Reference lookup
Load only the references this task needs. One level deep.
- Also read review-mode.md when reviewing a feature UI change.
- Also read ui-testing.md when changing UI behavior.
- Also read run-and-see.md before calling a visible change done.
- state-reads-and-stability.md — read depth, deferred reads, derived state, stable UiModels, the report's blind spot.
- ux-states.md — skeleton vs keep-content vs spinner, validation, disabled vs hidden.
- lists.md — keys, contentType, item-scope work, grids, nesting, paging hookup.
- motion.md — animation API choice, graphicsLayer, gestures.
- shared-elements.md — shared-element choice, keys, modifier order, overlay.
- accessibility.md — semantics, touch targets, contrast, actions, RTL.
- design-system.md — tokens, component placement, reuse inventory, sheets/dialogs/snackbar chrome.
- resources.md — CMP
Resvs AndroidR, qualifiers, locale parity, imports, maps. - resources-media.md — icons, fonts, raw files and remote content.
- images.md — Coil 3 setup and API choice, placeholders, caching, CMP placement.
- keyboard-and-focus.md — focus on arrival, IME actions, dismiss rules.
- modifiers.md — modifier order, custom
Modifier.Node, lambda (deferred-read) modifiers. - adaptive-layouts.md — adaptive shape per destination, size class not device, layout variants and naming, pane widths, scene-strategy panes.
- edge-to-edge-and-insets.md — edge-to-edge, insets exactly once, keyboard and IME.
- performance-diagnostics.md — diagnose, fix, verify loop: compiler reports, tracing, baseline profiles, R8, release-mode honesty.
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.

