santimattius/performance-compose-skills/compose-architecture
Claude Code and agent skill by santimattius/performance-compose-skills
- community
Checking that the SKILL.md file is still in the repository.
Jetpack Compose architecture patterns: Clean Architecture layering, MVVM/MVI with UiState/UiAction, Screen/Content split, collectAsStateWithLifecycle, stable UI models, and ViewModel scoping rules. Trigger: when structuring a Compose screen, wiring a ViewModel to UI, choosing MVVM vs MVI, handling UiState/UiAction patterns, or reviewing Compose architecture anti-patterns.
Install
npx vskill@latest install santimattius/performance-compose-skills/compose-architectureSKILL.md
Performance First
Every Compose frame runs three phases. Cost rises left-to-right; restart scope shrinks left-to-right.
| Phase | What Runs | Restart Cost | Trigger | |
|---|---|---|---|---|
| Composition | Composable functions, state reads | HIGH (whole scope) | State read in composable body | |
| Layout | Measure + place | MEDIUM (subtree) | State read in measure lambda | |
| Drawing | Canvas commands, graphicsLayer | LOW (single node) | State read in draw/graphicsLayer lambda |
Defer State Reads (decision tree)
Do you need the value during composition?
- YES → read as
T(Composition phase) — accept full restart cost - NO → can you defer to layout/draw?
- Layout-only? → pass
() -> TintoModifier.layout/offset { }lambda - Draw/transform? → pass
() -> TintoModifier.graphicsLayer { }/drawBehind { }lambda
Rule: prefer Modifier.offset { lambda } over Modifier.offset(state.value.dp). Lambda variant defers the read to the layout phase, skipping recomposition entirely.
Architecture Impact on Phases — collectAsStateWithLifecycle vs collectAsState
Performance Toolchain
WARNING: NEVER profile in debug builds. Debug disables R8, inlining, and Strong Skipping — numbers are meaningless. Always profile a release build withprofileableenabled or a benchmark build type.
| Tool | Use For | When | |
|---|---|---|---|
| Baseline Profiles | AOT-compile critical paths (startup, scroll) | Ship in release; regenerate per release | |
| Compose Compiler Reports | Detect unstable params, restartable/skippable status | Every PR; fail CI on new unstable types | |
| Layout Inspector (recomposition counts) | See which composables recompose and why | When debugging excess recomposition | |
| Composition Tracing | Frame-level composition timing in Android Studio | When Layout Inspector is not enough | |
| Macrobenchmark | Measure startup, frame timing, jank in release | Per-release regression gate |
When to Use
Load this skill when any of the following applies:
- Structuring a new Compose screen (Screen/Content split, state holder choice)
- Wiring a ViewModel to UI (
collectAsStateWithLifecycle,stateIn, UiState shape) - Choosing MVVM vs MVI for a feature
- Designing
UiStateorUiActiontypes (stability, sealed hierarchies) - Reviewing a screen for architecture anti-patterns (ViewModel in child, one-shot events, unstable state)
- Scoping ViewModels to navigation destinations
- Deciding where to hoist state and how low to read it
CMP Applicability
Canonical CMP rules: `../_shared/cmp-platform.md`
| Source set | Status | Notes | |
|---|---|---|---|
commonMain | ⚠️ | MVI/MVVM patterns ✅; Hilt ❌; ViewModel requires lifecycle-viewmodel-compose 2.10.0 | |
androidMain | ✅ | Full skill content applies; Hilt available here | |
iosMain | ⚠️ | ViewModel + StateFlow ✅ (lifecycle 2.10+); use Koin or expect/actual factory for DI | |
desktopMain | ⚠️ | Same; also requires kotlinx-coroutines-swing in jvmMain | |
wasmJsMain | ⚠️ | Same lifecycle version gate |
Status legend: ✅ fully supported · ⚠️ partial / version-gated · ❌ Android-only.
If using in CMP: Hilt is androidMain-only (kapt). For commonMain DI, use Koin (koin-compose-viewmodel) or a manual expect/actual factory. ViewModel and viewModelScope are multiplatform from org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose 2.10.0 — always provide an initializer: viewModel { MyViewModel() }. See `references/cmp-architecture-boundary.md`.
Critical Patterns
1. collectAsStateWithLifecycle — always, never collectAsState
From androidx.lifecycle:lifecycle-runtime-compose. Stops collecting when the lifecycle reaches STOPPED (app backgrounded, screen off), saving CPU and battery. collectAsState() collects forever regardless of lifecycle state.
This is a Strongly Recommended pattern in official Android docs — not optional.
In CMP: requiresandroidx.lifecycle:lifecycle-runtime-compose≥ 2.8.0 incommonMain. Below 2.8, usecollectAsState()or aexpect/actualshim. See references/cmp-architecture-boundary.md.
2. stateIn canonical pattern
The 5-second timeout keeps the pipeline alive through screen rotations without restarting. It stops when the UI is truly gone (not just rotated). NEVER use SharingStarted.Eagerly — the pipeline stays active even when no UI observes it, wasting CPU/battery.
3. No AndroidViewModel — use ViewModel with constructor injection
In CMP:ViewModelitself is multiplatform fromorg.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose2.10.0. Below 2.10, use theexpect/actualboundary in references/cmp-architecture-boundary.md.AndroidViewModelis Android-only and must never appear incommonMain.
4. No ViewModel instances in child composables
Pass only the state and lambdas that the child needs. Child composables receiving a ViewModel cannot be previewed, tested independently, or reused. Screen-level composables and navigation destinations are the ONLY valid ViewModel access points.
5. Events as state — never one-shot
Model navigation triggers, snackbars, and dialogs as Boolean or sealed class state fields in UiState, not as Channel or SharedFlow events. One-shot events are consumed and lost if the UI resubscribes; state is always replayable.
6. Stable UI models
All properties MUST be val. Use ImmutableList<T> from kotlinx.collections.immutable or an @Immutable wrapper for list fields. Types with var properties are always inferred unstable — composables receiving them can never be skipped.
7. Hoist state high, read state low
Hoist to the lowest common ancestor that needs to share state. But READ state as low in the tree as possible — pass () -> T lambdas to children for frequently-changing values so the read happens in the child's scope, not the parent's.
8. @Stable for plain class state holders
For reusable UI component state holders (not ViewModels), use a plain class annotated @Stable with MutableState properties. Do NOT use ViewModel for per-component state holders — they live too long and carry DI overhead.
9. Screen/Content split
One Screen composable accesses state and handles navigation callbacks (connects to ViewModel); one Content composable renders pure UI given immutable state. The Content composable is stateless, previewable, and testable in isolation.
10. _uiState.update { it.copy(...) } for atomic updates
Prevents intermediate inconsistent states under concurrent coroutines.
This is the start of the file. Read the full SKILL.md on GitHub.
Security
- Security scanPassed, 100 of 100Pattern checks on the skill's files.
- Model reviewNot runA model review is scheduled for the next rescan.
Quality evaluation
- Task evaluationNot runNo evaluation yet. Run one in Skill Studio.
Source
- SKILL.md in the repositoryCheckingskills/compose-architecture/SKILL.md
Popularity
- 7-day trend score
- 5
Not refreshed yet.
Works with
Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.
- Claude Code
- Cursor
- GitHub Copilot
- Windsurf
- Codex
- Gemini CLI
- Cline
- Amp
- Roo Code
- Goose
- Continue
- Kilo Code
- Junie
- Trae
- OpenHands
- Qwen Code
- AI Agent
- Tabnine Chat
- Sourcegraph Cody
- Amazon Q
- Firebase Genkit
- Sweep
- Devin
- Phind
- Mentat
- Claude Desktop
- Aider
- GPT Pilot
- Plandex
- bolt.new
- v0
- Replit Agent
- Anse
- TypingMind
- BetterChatGPT
- LibreChat
- Warp AI
- Pieces
- Fig AI
Embed badge
[](https://verified-skill.com/skills/santimattius/performance-compose-skills/compose-architecture)