compose-clean-code
Skill0 starsChanged 59 days agoMITView on GitHub
Enforces strict architecture, performance, Material 3 theming, and quality standards for Jetpack Compose (API 33+). Mandates hard limits on composable size/nesting/params, stateless screen/route separation, immutable state collections, zero side effects in composition bodies, zero emojis (always Google Material theme icons), type-safe navigation, and Compose linting/detekt enforcement.
In this repository20 other files it gives its agents.
- AGENTS.mdAGENTS.md
- rules/AGENTS.mdAGENTS.md
- kurokoSkill
- tayoriSkill
- xerjSkill
- aesthetic-readme-craftSkill
- cli-builderSkill
- codebase-digestSkill
- git-release-craftSkill
- git-repo-craftSkill
- karakuriSkill
- python-clean-codeSkill
- rust-clean-codeSkill
- rust-style-pythonSkill
- systematic-code-verificationSkill
- bash-clean-codeSkill
- tui-designSkill
- frontend-designSkill
- web-design-guidelinesSkill
- unslopSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
39 rules. About 0 are common, 9 are distinctive, and 30 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈9
Distinctive · estimate
This author's own choices. Worth a look.
30
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 32
Never: use raw emojis as UI icons, button graphics, or status indicators.
- Very specific: line 33
Always: use official Google Material Theme icons (Icons.Rounded.* or Icons.Outlined.* from androidx.compose.material.icons) or Material …
- Distinctive: line 34
If no specific styling or icon is explicitly defined in the prompt, always default to standard Material theme icons.
- Very specific: line 38
Every composable emitting layout must accept modifier: Modifier = Modifier as its first optional parameter, applied strictly to the root …
- Very specific: line 40
Only the screen-level XRoute touches the ViewModel.
- Very specific: line 42
Never pass MutableList, ArrayList, or mutable maps.
- Very specific: line 44
Never call mutableStateOf inside a composable without wrapping it in remember or rememberSaveable.
- Distinctive: line 46
Never launch coroutines, register listeners, or log analytics directly in the composition body.
- Very specific: line 48
Never use GlobalScope or instantiate CoroutineScope(...) inside a composable.
- Very specific: line 52
Never suppress ComposableNaming, ModifierMissing, ModifierNotUsedAtRoot, etc.
- Distinctive: line 54
Keep previews actively maintained and runnable, targeting stateless screen representations.
- Very specific: line 58
Never nest a LazyColumn inside a verticalScroll container.
- Very specific: line 86
Always use collectAsStateWithLifecycle(), never bare collectAsState() for UI-facing flows.
- Very specific: line 95
Use rememberSaveable for user inputs, scroll positions, and selected tabs to survive process death and recreation.
- Very specific: line 97
Use derivedStateOf only when the derived calculation changes less often than its inputs (e.g.
- Very specific: line 114
Never keep an effect key of true or a constant to fake "run once" unless truly intended once-per-composition lifetime.
- Very specific: line 133
Prefer method references (viewModel::onEvent) or remember-ed lambdas to prevent reallocations and unnecessary recompositions.
- Very specific: line 135
Never allocate objects in the composition body on hot paths (listOf(...), Modifier chains with lambdas, Brush, Paint) without remember.
- Distinctive: line 137
Never write to state that was already read in the same composition pass.
- Very specific: line 166
Use SwipeToDismissBox with state remembered per item key.
- Very specific: line 168
Use Modifier.animateItem() (or animateItemPlacement on older Compose) for smooth reordering and insertion animations.
- Very specific: line 170
Never place a LazyColumn inside a scrollable container of unbounded height.
- Very specific: line 188
Use semantic color tokens (primary, onPrimary, surfaceContainer, onSurfaceVariant), never raw color palettes or hardcoded hex values.
- Very specific: line 190
Use official Material 3 components (Button, FilledTonalButton, Card, NavigationBar, TopAppBar, ModalBottomSheet).
- Very specific: line 194
Call enableEdgeToEdge() in the host Activity.
- Very specific: line 196
Use MaterialTheme.typography.* styles with sizes in sp.
- Distinctive: line 198
Zero Emojis: Never use emojis in UI layouts or controls.
- Very specific: line 199
Material Icons: Always use Icons.Rounded or Icons.Outlined from androidx.compose.material.icons (or dedicated vector drawables).
- Distinctive: line 200
Default Styling: If no specific styling or icon is defined in the prompt, always default to standard Material theme icons.
- Very specific: line 210
Use Compose BackHandler or PredictiveBackHandler rather than overriding Activity onBackPressed.
- Very specific: line 216
Use ActivityResultContracts.PickVisualMedia instead of requesting READ_EXTERNAL_STORAGE.
- Very specific: line 225
Use @Serializable data classes and objects for routes instead of string paths and bundle parsing:
- Very specific: line 243
Prefer animate*AsState, AnimatedVisibility, AnimatedContent, and updateTransition over manual Animatable unless handling direct gesture …
- Very specific: line 256
Every stateless XScreen must provide preview variations for:
- Very specific: line 269
Test ViewModels using runTest and Turbine on uiState, configuring Dispatchers.setMain(StandardTestDispatcher()).
- Very specific: line 278
Every icon button and interactive element must provide a clear contentDescription (or null if purely decorative).
- Very specific: line 280
All interactive elements must satisfy minimum touch target sizing (≥ 48dp) via Modifier.minimumInteractiveComponentSize().
- Distinctive: line 284
Never convey state, selection, or status by color alone.
- Distinctive: line 286
Verify layout integrity with TalkBack enabled and system font scaling set to 200%.
≈ “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-clean-code Skill: Jetpack Compose Architecture, Performance & Quality Standards
Compose-only companion to general Kotlin & Android development rules (package structure, CI, and general Kotlin rules). Material 3, API 33 baseline. Drop in as CLAUDE.md, .cursor/rules, or RULES.md.
1. Hard Limits
| Metric | Limit | Action if Exceeded |
| Lines per composable | 40 soft, 60 hard | Extract sub-composables or child presentation components |
| Lines per file | 300 soft, 400 hard | Split into submodules, feature files, or helper files |
| Composables per file | 6–8 | Screen composable + private helper composables only |
| Composable parameters | 5 max (excluding modifier) | Group into a dedicated immutable State or Config class |
| Nesting depth in one composable body | 3 levels | Extract child composable at level 3 |
| Modifier chain length | 6 calls | Extract a named Modifier extension function |
2. Zero Tolerance
- Zero Emojis in UI / Layout:
- Never use raw emojis as UI icons, button graphics, or status indicators.
- Always use official Google Material Theme icons (
Icons.Rounded.*orIcons.Outlined.*fromandroidx.compose.material.icons) or Material vector drawables. - If no specific styling or icon is explicitly defined in the prompt, always default to standard Material theme icons.
- No Business Logic, IO, or Calculation in Composition:
- Composables render state and emit events—nothing else. Heavy calculations belong in ViewModel/Domain layers.
- Missing
modifier: Modifier = Modifier:- Every composable emitting layout must accept
modifier: Modifier = Modifieras its first optional parameter, applied strictly to the root layout element.
- Every composable emitting layout must accept
- Passing
ViewModelinto Child Composables:- Only the screen-level
XRoutetouches theViewModel. Child composables receive plain immutable state and lambda callbacks.
- Only the screen-level
- Mutable Collections in State or Parameters:
- Never pass
MutableList,ArrayList, or mutable maps. UseImmutableList/PersistentList(kotlinx.collections.immutable) or an@Immutablewrapper.
- Never pass
mutableStateOfwithoutremember:- Never call
mutableStateOfinside a composable without wrapping it inrememberorrememberSaveable.
- Never call
- Side Effects in Composition Body:
- Never launch coroutines, register listeners, or log analytics directly in the composition body. Use effect APIs (§4).
GlobalScopeor UnboundCoroutineScope:- Never use
GlobalScopeor instantiateCoroutineScope(...)inside a composable. UserememberCoroutineScope()for UI event handlers.
- Never use
- Hardcoded Colors, Dimensions, or Raw Strings:
- No
Color(0x...), raw.dp/.spspacing literals, or hardcoded strings outsideMaterialTheme,Dimens, orstringResource().
- No
@Suppresson Compose Lint Rules:- Never suppress
ComposableNaming,ModifierMissing,ModifierNotUsedAtRoot, etc. Fix the underlying code.
- Never suppress
- Stale or Unused
@PreviewFunctions:- Keep previews actively maintained and runnable, targeting stateless screen representations.
- Lazy Lists Without Stable Keys:
LazyColumnandLazyRowitems must define a stablekey(andcontentTypefor heterogeneous lists). Never use list index as key.
- Nested Scrollables in Same Direction:
- Never nest a
LazyColumninside averticalScrollcontainer. Use a single lazy list with multiple item types.
- Never nest a
3. State Rules
Screen / ViewModel Contract:
@Composable
fun TasksRoute(viewModel: TasksViewModel = hiltViewModel()) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
TasksScreen(state = state, onEvent = viewModel::onEvent)
}
@Composable
private fun TasksScreen(
state: TasksUiState,
onEvent: (TasksEvent) -> Unit,
modifier: Modifier = Modifier,
) {
/* stateless, previewable */
}- Two Layers Per Screen:
XRoute: Wires ViewModel, dependency injection, and collects state.XScreen: Completely stateless, receives UI state and emits event lambdas. Previews and tests targetXScreen.
- Lifecycle-Aware State Collection:
- Always use
collectAsStateWithLifecycle(), never barecollectAsState()for UI-facing flows.
- Always use
- Single Source of Truth:
- One
UiStatesealed interface or data class per screen exposed asStateFlow<UiState>. - One
UiEventsealed interface going back to the ViewModel, or individual lambdas for simple screens.
- One
- One-Shot Effects:
- Navigation, snackbars, and toasts must flow via
ChannelorSharedFlowconsumed inLaunchedEffect, never as transient boolean flags inUiStatethat require manual reset.
- Navigation, snackbars, and toasts must flow via
- State Hoisting:
- Hoist state to the lowest common parent that reads it. Local UI-only state (focus, transient expanded state) stays local using
rememberorrememberSaveable.
- Hoist state to the lowest common parent that reads it. Local UI-only state (focus, transient expanded state) stays local using
- Process Death & Configuration Resiliency:
- Use
rememberSaveablefor user inputs, scroll positions, and selected tabs to survive process death and recreation.
- Use
- Optimized Derivation (
derivedStateOf):- Use
derivedStateOfonly when the derived calculation changes less often than its inputs (e.g.listState.firstVisibleItemIndex > 0). Never wrap trivial calculations.
- Use
- Late State Reading:
- Defer reading state until the layout or draw phase by passing lambdas (
{ scrollOffset }) or usingModifier.graphicsLayer {}/Modifier.drawBehind {}for high-frequency updates (e.g. scroll offsets, animations).
- Defer reading state until the layout or draw phase by passing lambdas (
4. Side Effects API Matrix
| Need | API | Rules & Guardrails |
| Suspend work on key change / enter | LaunchedEffect(key) | Never use LaunchedEffect(Unit) when body depends on changing state; key on the dependency. |
| Launch coroutine from UI event | rememberCoroutineScope() | For click/gesture callbacks only; never launch during composition pass. |
| Register / unregister listeners | DisposableEffect(key) | Always implement onDispose { } cleanup. |
| Push Compose state to non-Compose objects | SideEffect { } | Runs after every successful recomposition. |
| Convert non-Compose data to Compose state | produceState(...) / collectAsStateWithLifecycle | Bridges external asynchronous data sources to Compose state. |
| Capture latest lambda in long-lived effect | rememberUpdatedState(newValue) | Prevents restarting long-running effects when a callback changes. |
- Never keep an effect key of
trueor a constant to fake "run once" unless truly intended once-per-composition lifetime.
5. Recomposition & Stability
- Stable Parameters:
- Composable parameters must be stable: primitives,
String, function types,@Immutable/@Stableclasses, and immutable collections (ImmutableList). - Unstable parameters (e.g. standard
java.util.List, external third-party classes) force recomposition of the callee.
- Composable parameters must be stable: primitives,
- Strong Skipping Mode & Compiler Metrics:
- Enable strong skipping mode (default in Kotlin 2.0.20+). Audit with compiler reports:
composeCompiler { reportsDestination = layout.buildDirectory.dir("compose_reports") metricsDestination = layout.buildDirectory.dir("compose_metrics") } - Review
*-composables.txtto eliminaterestartablebut notskippablecomposables on hot paths.
- Enable strong skipping mode (default in Kotlin 2.0.20+). Audit with compiler reports:
- Lambda Stability:
- Prefer method references (
viewModel::onEvent) orremember-ed lambdas to prevent reallocations and unnecessary recompositions.
- Prefer method references (
- Allocation Prevention on Hot Paths:
- Never allocate objects in the composition body on hot paths (
listOf(...),Modifierchains with lambdas,Brush,Paint) withoutremember.
- Never allocate objects in the composition body on hot paths (
- Zero Backwards Writes:
- Never write to state that was already read in the same composition pass.
6. Lists & Scrolling
LazyColumn(contentPadding = PaddingValues(Dimens.md)) {
items(
items = tasks,
key = { it.id },
contentType = { it.type }
) { task ->
TaskRow(
task = task,
modifier = Modifier.animateItem()
)
}
}- Stable Keys & Content Types:
keymust be a stable, unique ID (never list index).- Provide
contentTypewhen items have heterogeneous layouts to enable view recycling.
- State Hoisting:
- Hoist
rememberLazyListState()when parent controls need scroll position or list control.
- Hoist
- Item Extraction for Skipping:
- Extract heavy item contents into standalone composables so Compose can skip unchanged rows individually.
- Swipe-to-Dismiss:
- Use
SwipeToDismissBoxwith state remembered per item key. Confirm changes inconfirmValueChange, not by mutating collections in composition.
- Use
- Item Animations:
- Use
Modifier.animateItem()(oranimateItemPlacementon older Compose) for smooth reordering and insertion animations.
- Use
- Zero Unbounded Height Scrolling:
- Never place a
LazyColumninside a scrollable container of unbounded height.
- Never place a
7. Material 3 & Theming
- Root Theme Hierarchy:
- One
AppTheme { }wrappingMaterialThemeat the application root. Child composables consumeMaterialTheme.colorScheme,typography, andshapes.
- One
- Dynamic Color Support (Android 12+):
val colorScheme = when { dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) darkTheme -> DarkColors else -> LightColors } - Semantic Roles:
- Use semantic color tokens (
primary,onPrimary,surfaceContainer,onSurfaceVariant), never raw color palettes or hardcoded hex values.
- Use semantic color tokens (
- Standard M3 Components:
- Use official Material 3 components (
Button,FilledTonalButton,Card,NavigationBar,TopAppBar,ModalBottomSheet). Wrap incore/designsystem/components/only when establishing an app-wide variant.
- Use official Material 3 components (
- Design Tokens & Spacing Scale:
- Centralize spacing in a
Dimensobject (xs = 4.dp,sm = 8.dp,md = 16.dp,lg = 24.dp,xl = 32.dp). Zero stray dp literals.
- Centralize spacing in a
- Edge-to-Edge Compliance:
- Call
enableEdgeToEdge()in the host Activity. Handle system bars and insets viaScaffoldcontent padding orWindowInsetsmodifiers.
- Call
- Typography & Scalable Text:
- Use
MaterialTheme.typography.*styles with sizes insp. Never wrap text in fixed-height containers that clip during font scaling.
- Use
- Iconography Standards:
- Zero Emojis: Never use emojis in UI layouts or controls.
- Material Icons: Always use
Icons.RoundedorIcons.Outlinedfromandroidx.compose.material.icons(or dedicated vector drawables). - Default Styling: If no specific styling or icon is defined in the prompt, always default to standard Material theme icons.
8. Android 13 (API 33) Specifics
- Notification Permission (
POST_NOTIFICATIONS):- Request at runtime at the contextual moment the user enables notifications/reminders (never on initial launch) via
rememberLauncherForActivityResultorrememberPermissionState. Provide clear rationale on denial.
- Request at runtime at the contextual moment the user enables notifications/reminders (never on initial launch) via
- Predictive Back Navigation:
- Opt in via
android:enableOnBackInvokedCallback="true"inAndroidManifest.xml. - Use Compose
BackHandlerorPredictiveBackHandlerrather than overriding ActivityonBackPressed.
- Opt in via
- Themed App Icon:
- Provide a monochrome layer in the adaptive icon definition for Material You dynamic theming.
- Per-App Language Preferences:
- Supply
LocaleConfigand manage locales viaAppCompatDelegate.setApplicationLocales.
- Supply
- Modern Photo Picker:
- Use
ActivityResultContracts.PickVisualMediainstead of requestingREAD_EXTERNAL_STORAGE.
- Use
- API Version Gating:
- Guard any API call above API 33 behind explicit
Build.VERSION.SDK_INT >= Build.VERSION_CODES...checks.
- Guard any API call above API 33 behind explicit
9. Type-Safe Navigation
- Navigation Compose 2.8+ Type-Safe Routes:
- Use
@Serializabledata classes and objects for routes instead of string paths and bundle parsing:@Serializable data class TaskDetailRoute(val taskId: String)
- Use
- Feature Navigation Encapsulation:
- Define routes per feature in
feature/<name>/ui/<Name>Navigation.kt, exposingNavGraphBuilder.<name>Screen(...)andNavController.navigateTo<Name>(...). - The top-level
NavHostonly aggregates these modular extension functions.
- Define routes per feature in
- Zero NavController Leaks:
- Composables and screens must never receive or hold a
NavControllerreference. Screens receive pure lambda callbacks (onNavigateToDetail: (String) -> Unit).
- Composables and screens must never receive or hold a
- Pass IDs, Not Complex Data Models:
- Pass lightweight IDs across navigation routes. The destination screen's ViewModel loads its own data.
10. Animations
- High-Level Declarative APIs:
- Prefer
animate*AsState,AnimatedVisibility,AnimatedContent, andupdateTransitionover manualAnimatableunless handling direct gesture tracking.
- Prefer
- Centralized Animation Specs:
- Durations, easings, and spring specs come from centralized animation constants, not per-call literals.
- Render Phase Animations:
- Execute continuous animations in the draw or layout phase (
Modifier.graphicsLayer {},Modifier.offset {}) to avoid triggering recomposition on every frame.
- Execute continuous animations in the draw or layout phase (
- Reduced Motion Support:
- Respect system animation scaling and user accessibility preferences when rendering decorative animations.
11. Previews & Testing
- Comprehensive Screen Previews:
- Every stateless
XScreenmust provide preview variations for:- Loading state
- Content populated state
- Empty state
- Error state
- Include dark-theme and large-font (
fontScale = 1.5f) previews for complex layouts.
- Every stateless
- Multipreview Annotations:
- Define custom multipreview annotations (
@PreviewLightDark,@PreviewFontScale,@PreviewScreenSizes) to avoid redundant annotation blocks.
- Define custom multipreview annotations (
- Fake Preview Data:
- Preview data must originate from static
PreviewDataproviders or mock classes, never real network or repository calls.
- Preview data must originate from static
- Semantics-First UI Testing:
- Target
XScreenwithcreateComposeRule(). Query nodes by accessibility semantics (onNodeWithText,onNodeWithContentDescription). UsetestTagstrictly as a last resort.
- Target
- ViewModel State Testing:
- Test ViewModels using
runTestandTurbineonuiState, configuringDispatchers.setMain(StandardTestDispatcher()).
- Test ViewModels using
- Screenshot Regression Testing:
- Maintain Paparazzi or Roborazzi snapshot tests for core design system components.
12. Accessibility (a11y)
- Descriptive Clickables:
- Every icon button and interactive element must provide a clear
contentDescription(ornullif purely decorative).
- Every icon button and interactive element must provide a clear
- Minimum Touch Targets:
- All interactive elements must satisfy minimum touch target sizing (≥ 48dp) via
Modifier.minimumInteractiveComponentSize().
- All interactive elements must satisfy minimum touch target sizing (≥ 48dp) via
- Semantic Grouping:
- Group related content using
Modifier.semantics(mergeDescendants = true). Clear decorative clutter withclearAndSetSemantics.
- Group related content using
- Multi-Modal State Indication:
- Never convey state, selection, or status by color alone. Combine color with text labels or iconography.
- Scaling Audits:
- Verify layout integrity with TalkBack enabled and system font scaling set to 200%.
13. Lint & Automated Enforcement
Configure Compose-specific linting in build scripts:
// build.gradle.kts
dependencies {
detektPlugins("io.nlopez.compose.rules:detekt:<latest>")
lintChecks("com.slack.lint.compose:compose-lint-checks:<latest>")
}Mandatory Error-Level Rules:
ModifierMissingModifierNotUsedAtRootModifierReusedViewModelForwardingMutableParamsUnstableCollectionsComposableNamingLambdaParameterInRestartableEffectRememberMissingContentEmitterReturningValuesMultipleEmitters
CI Grep Gates:
# Prohibit bare collectAsState
grep -rn "collectAsState()" --include="*.kt" app/src/main && exit 1
# Prohibit GlobalScope
grep -rn "GlobalScope" --include="*.kt" app/src/main && exit 1
# Prohibit raw hex color instantiation outside design system
grep -rnE "Color\(0x" --include="*.kt" app/src/main | grep -v "designsystem/Color.kt" && exit 114. Pre-PR Verification Checklist
- Architecture: Screen cleanly split into
XRoute(ViewModel wiring) and statelessXScreen. - Modifiers: Every layout composable provides
modifier: Modifier = Modifieras first-optional param, applied to root element only. - Zero Logic in Composition: No business logic, IO, calculations, or coroutine launches in the composition body.
- Zero Emojis: No emojis used for icons, buttons, or indicators; Google Material Theme icons (
Icons.Rounded/Outlinedor vector drawables) used exclusively. - Default Icon Styling: Standard Google Material Theme icons used when no explicit custom styling is requested.
- Lazy Lists: Stable
key(andcontentTypewhere applicable) provided; no nested same-direction scroll containers. - Stability: Composable parameters are stable or
@Immutable;collectAsStateWithLifecycle()used on UI flows. - Design Tokens: All colors, typography styles, spacing, and strings sourced from
MaterialTheme,Dimens, orstringResource. - Previews: Stateless
XScreencovered by content, loading, empty, error, dark theme, and font scale previews. - Accessibility: Touch targets ≥ 48dp; all interactive icons possess meaningful
contentDescription. - Linters & CI: Zero Compose lint or Detekt warnings, zero
@Suppressannotations, CI grep gates pass. - Dead Code: No unused composables, orphaned models, or stale previews.
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.

