A strict, evidence-based audit skill for Android Jetpack Compose repositories.
npx skills add https://github.com/hamen/compose_skill --skill jetpack-compose-auditقم بتثبيت هذه المهارة باستخدام واجهة سطر الأوامر (CLI) وابدأ في استخدام سير عمل SKILL.md في مساحة عملك.
Version 1.3 · released 2026-04-22
Find out where your Compose app is burning frames, by how much, and what to change to win them back — measured against real compiler data, not vibes.
A strict, evidence-based audit for Android Jetpack Compose repositories. Point it at a repo, let it run the build once, and get back a 0-100 score, a 0-10 score per category, an actionable top-three fix list, and a full Markdown report with every deduction cited against an official developer.android.com page.
Built for Claude Code, Cursor, and any agent that loads the Anthropic skill format.
Added — Cursor plugin support.
Same-day follow-up to 1.2. Cursor's plugin system is structurally identical to Claude Code's (hidden manifest directory, single-skill plugins auto-discover a root-level SKILL.md), so shipping a sidecar manifest was essentially free — and it unblocks org admins who want to import the skill into their team marketplace via Cursor's GitHub-import flow.
.cursor-plugin/plugin.json at the repo root. Same metadata as the Claude Code manifest (name, version, description, author, repository, license, keywords). The skills field is intentionally omitted so Cursor treats the repo as a single-skill plugin and auto-discovers SKILL.md at the root — matching the documented default behaviour..claude-plugin/plugin.json bumped to 1.3.0 to match. Both plugin manifests and SKILL.md now read the same version string.Added — Claude Code plugin support.
The skill now ships a .claude-plugin/plugin.json manifest, so Claude Code users can install it directly from the Git URL via /plugin add hamen/compose_skill instead of cloning and symlinking by hand. Nothing else about the skill changed — same rubric, same categories, same report template.
.claude-plugin/plugin.json at the repo root. Declares the skill name, version, repository, license, and keywords, and points Claude Code at the existing SKILL.md via "skills": "./". Purely additive — no existing files were restructured.Refined — Strong Skipping-aware scoring, detection, and reporting.
This is a corrective follow-up to 1.1. After feedback around Strong Skipping Mode, the skill now evaluates modern Compose repos the way the compiler actually behaves on Kotlin 2.0.20+ / Compose Compiler 1.5.4+.
skippable% and unstable shared params. Under Strong Skipping, the audit no longer blindly caps scores because a repo uses raw List params or misses @Stable on its own.listOf(...), mapOf(...), fresh UI models, object / lambda literals in composable bodies), expensive or broken equals() on unstable params, and unjustified @NonSkippableComposable / @DontMemoize opt-outs on hot paths.skippable% from named-only skippable%, because zero-argument lambdas can artificially drag down the raw module number. Reports are instructed to say which metric actually bound the ceiling.ImmutableList / PersistentList are no longer framed as a mandatory cargo-cult fix under Strong Skipping. They still earn credit, but for the right reasons: structural sharing, predictable equality, and lower churn when collections are reused deliberately.List params or missing @Stable, and what to inspect instead when SSM is active.skippable%, unstable shared-type count, the actual binding cap, and the final applied score.equals(), or clearing the binding cap rather than promising a magic skippable% jump.Net effect: fewer false positives on modern Compose codebases, fewer cargo-cult recommendations, and more defensible audit reports when the repo already runs with Strong Skipping enabled by default.
Added — animation auditing.
.value reads piped into state-reading modifiers (Modifier.offset(x.dp), Modifier.alpha(a)) when lambda-form modifiers (Modifier.graphicsLayer { ... }, Modifier.offset { ... }) would defer to layout/draw. Flags Animatable(...) created in a composable body without remember { ... } or hoisting. Flags rememberInfiniteTransition() hosted in composables that stay composed offscreen. Adds API-choice guidance for Crossfade vs AnimatedContent: standard fades remain fine with Crossfade, while custom enter/exit or size-aware swaps belong in AnimatedContent.Animatable animations launched from the composition body. Flags rememberCoroutineScope().launch { animatable.animateTo(...) } when the animation is target-driven and LaunchedEffect(target) is the clearer fit.animationSpec: AnimationSpec<T> on shared animated APIs when callers may reasonably need timing control. Treats missing label parameters on tooling-visible shared animations as a light tooling-quality smell rather than a hard failure.animation/introduction, animation/value-based, animation/customize) to the URL ledger every deduction must cite.scripts/compose-reports.init.gradle).skippable% ceilings on the Performance score.developer.android.com or the AndroidX component guidelines.COMPOSE-AUDIT-REPORT.md with prioritized fixes.Run the skill on a Compose repo and you walk away with:
COMPOSE-AUDIT-REPORT.md written at the target root — per-category scoring, evidence file paths, line numbers, and prioritized fixes.skippable%, named-only skippable%, the unstable-class list, and the per-module Strong Skipping state inferred from compiler version plus explicit flags.Four categories, weighted for an app repo. Each scored 0-10; overall on 0-100.
| Category | Weight | What it covers |
|---|---|---|
| Performance | 35% | Work in composition, lazy-list keys, state-read timing, stability, Strong Skipping, backwards writes, animation phase correctness, baseline profiles |
| State management | 25% | Hoisting, single source of truth, rememberSaveable, lifecycle-aware collection, observable collections, ViewModel placement, type-safe navigation |
| Side effects | 20% | Effect API choice, keys, stale captures, cleanup, composition-time work, animation driving via LaunchedEffect |
| Composable API quality | 20% | Modifier conventions, parameter order, slot APIs, CompositionLocal usage, Modifier.Node, animationSpec exposure, @Preview coverage, hardcoded strings / magic numbers |
Score bands: 0-3 fail · 4-6 needs work · 7-8 solid · 9-10 excellent.
Concrete smells the rubric targets, with realistic wins:
| Smell | Expected gain after fix |
|---|---|
Unstable or repeatedly recreated params (List, domain models, ArrayList-backed state, listOf(...), fresh UI models) |
On older compiler tracks, can lift named-only skippable% and the Performance ceiling. Under Strong Skipping, usually removes instance-recreation churn or expensive equals() work that was still forcing re-runs despite high skippability. |
Lazy-list items(...) without stable key = |
Fewer reallocated compositions on reorder, smoother scroll, fewer IllegalArgumentException: Key already used crashes |
| Rapidly-changing state read high in the tree | Recompositions collapse from "per frame, whole screen" to "per frame, single modifier" |
Animated .value piped into Modifier.offset(x.dp) / Modifier.alpha(a) |
Moving to Modifier.graphicsLayer { ... } / Modifier.offset { ... } defers per-frame reads to layout/draw — same animation, fraction of the recomposition cost |
Animatable(...) created in a composable body without remember |
Animation no longer resets on every recomposition; velocity and target survive |
rememberCoroutineScope().launch { animatable.animateTo(...) } for target-driven animation |
Replace with LaunchedEffect(target) — restart semantics follow the target automatically, while rememberCoroutineScope() stays available for event-driven animation |
rememberInfiniteTransition hosted on something that stays composed offscreen |
Scoping it to visible content avoids needless offscreen animation work and lets it stop when the host actually leaves composition |
collectAsState() on Android UI flows |
Swap to collectAsStateWithLifecycle() — no collection when UI is paused |
mutableStateOf<Int> / <Long> / <Float> in hot paths |
Remove autoboxing, fewer allocations |
| Hardcoded strings and magic numbers in reusable components | i18n + dark-mode + accessibility ready; testable |
rememberSaveable inside a LazyListScope item factory |
No more TransactionTooLargeException when the list grows |
Scaffold { innerPadding -> ... } content that ignores innerPadding |
Content stops drawing behind the TopAppBar / system bars |
The report lists every occurrence with file path and line number, not just the category.
Measured, not inferred. The skill ships scripts/compose-reports.init.gradle and injects it into your Gradle build via --init-script — no edits to your build.gradle. Every run parses real *-classes.txt / *-composables.txt / *-module.json output.
Mandatory ceilings. A Performance score cannot exceed the cap set by the matching ceiling table. On older compiler tracks the cap is driven by skippable% plus unstable-param count; under Strong Skipping it is driven by named-only skippable%, instance-recreation churn, and equals() quality on unstable params. The ceiling math appears in the report so the score is auditable.
Every deduction cites an official source. Each finding carries a References: line pointing at developer.android.com or the AndroidX component API guidelines. Audits that can't be defended with a URL don't ship.
Actionable chat summary. The chat output mirrors the report's Prioritized Fixes — same file paths, same doc links, same predicted impact ("stops rebuilding FeedItemUiModel, removes the Strong-Skipping cap from 8 → no cap").
Install directly from the Git repository — no cloning, no symlinking:
/plugin add hamen/compose_skill
Claude Code reads .claude-plugin/plugin.json and registers SKILL.md automatically. Updates arrive via the normal plugin update flow.
The repo ships a .cursor-plugin/plugin.json manifest, so Cursor sees it as a valid single-skill plugin. There are two install paths today:
/plugin add-style command for arbitrary Git URLs yet; symlink install remains the practical path until this skill is published to the public Cursor Marketplace.Use this on Cursor, on older Claude Code versions without /plugin add, or whenever you want git pull in this directory to update the skill in place:
# Claude Code
mkdir -p ~/.claude/skills
ln -s "$(pwd)" ~/.claude/skills/jetpack-compose-audit
# Cursor
mkdir -p ~/.cursor/skills
ln -s "$(pwd)" ~/.cursor/skills/jetpack-compose-audit
From the agent prompt:
/jetpack-compose-audit [repo path or module path]
Or in natural language:
Audit this Compose repo.
Score the :app module for Compose quality.
Run a Compose performance review on core/ui.
The compiler-report build runs automatically and typically takes 1-5 minutes. If the build fails (no wrapper, compile error, timeout) the skill falls back to source-inferred findings, caps Performance at 7, and flags reduced confidence — all stated explicitly in the report.
Overall: 73/100
Performance: 8/10 capped by the SSM-on table: instance-recreation churn in feed params (qualitative 9)
State: 6/10 collectAsState without lifecycle, duplicate VM reads
Side effects: 7/10 LaunchedEffect key too broad at HomeScreen.kt:240
API quality: 8/10 BoxCard / SearchBar follow conventions
Compiler:
Strong Skipping: on (default)
ceiling table: SSM-on
module-wide skippable% = 186/269 = 69.14%
named-only skippable% = 121/122 = 99.18%
ceiling metric: named-only `skippable%` (module-wide metric anchored by zero-arg lambdas)
deferredUnstableClasses: 59
binding cap: 8 (fresh `FeedItemUiModel(...)` + `listOf(...)` rebuilt in `HomeFeedScreen`)
Top 3 fixes
1. collectAsState -> collectAsStateWithLifecycle across 6 call sites
feature/home/HomeScreen.kt:37, MainActivity.kt:213, ...
Doc: developer.android.com/.../side-effects
Impact: fewer redundant collections, lifecycle-correct
2. Stop rebuilding `FeedItemUiModel(...)` and `listOf(...)` inside `HomeFeedScreen`
Evidence: app/build/compose_audit/app_release-classes.txt, feature/home/HomeFeedScreen.kt:88-132
Doc: developer.android.com/.../stability
Impact: removes forced re-runs under Strong Skipping, likely clears the Performance cap from 8 -> no cap
3. Narrow LaunchedEffect(homeScreenState) at HomeScreen.kt:240-254
Doc: developer.android.com/.../side-effects
Impact: fewer redundant ensureAuthenticated() calls
In scope (1.x). Jetpack Compose on Android, Kotlin 2.0.20+ / Compose Compiler 1.5.4+ (Strong Skipping default).
Out of scope (1.x) — the skill will call these out as a note rather than silently produce thin coverage:
material-3 skill.expect/actual, target-specific code paths).SKILL.md main skill manifest (process, principles, output)
scripts/
compose-reports.init.gradle Gradle init script injected via --init-script
references/
scoring.md rubric with measured ceilings and inline citations
search-playbook.md grep patterns, regex, read-the-file heuristics
canonical-sources.md every URL the rubric cites
report-template.md required structure for COMPOSE-AUDIT-REPORT.md
diagnostics.md manual-mode fallback snippets
Prioritized Fixes section and the chat summary mirror each other, so the developer can act on the chat alone.MIT.