Android Kotlin — Compose UI Module
Parent kit: ../../SKILL.md · Index: ../../AGENTS.md · Banned list: ../../references/00-banned-antipatterns.md
REVIEW MODE
Trigger phrases: "audit this file", "check this composable", "review this screen", "review mode", "a11y pass on this composable".
When triggered, silently run this 6-point checklist on the target file before responding. Cite failures by checklist number and banned-antipattern row from ../../references/00-banned-antipatterns.md.
| # | Check | Fail if |
|---|---|---|
| 1 | Modifier param | Missing modifier: Modifier = Modifier or not last optional param |
| 2 | State hoisting | Screen/business state inside @Composable that belongs in ViewModel |
| 3 | Lazy list keys | LazyColumn/LazyRow items missing key = {} or contentType = {} |
| 4 | Lifecycle collection | Uses collectAsState() instead of collectAsStateWithLifecycle() |
| 5 | Side effects in composition | IO, DB, network, or viewModelScope work directly in composable body |
| 6 | Stability | Data class params to child composables lack @Stable or @Immutable |
Output format for REVIEW MODE:
## Compose audit — <filename> | # | Status | Finding | |---|--------|---------| | 1 | PASS/FAIL | ... | ... ### Banned antipatterns hit - Row N: <pattern> → <fix> ### Suggested diff <code block with minimal fix>
Load ../../references/17-accessibility.md when audit includes TalkBack, touch targets, or contrast.
AUTHORING MODE
Default behavior when writing new Compose UI (not auditing). Follow all rules below.
Read First
| File | When |
|---|---|
../../references/02-compose-ui.md | Stability, slots, Material 3, CompositionLocal themes |
../../references/03-animations.md | Springs, Canvas, gestures |
../../references/10-performance.md | derivedStateOf, LazyColumn keys, baseline profiles |
../../references/16-coil-image.md | AsyncImage, Coil 3.4 network dep |
../../references/17-accessibility.md | Semantics, 48dp targets, WCAG |
Edge-to-Edge (Default — AGP 9 / API 35+)
kotlinclass MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { enableEdgeToEdge() super.onCreate(savedInstanceState) setContent { AppTheme { Scaffold( modifier = Modifier.fillMaxSize(), contentWindowInsets = WindowInsets(0, 0, 0, 0) ) { innerPadding -> NavHost( modifier = Modifier .fillMaxSize() .padding(innerPadding) ) } } } } }
enableEdgeToEdge()on everyComponentActivity- Respect
WindowInsets— never hardcode status bar padding - Use
WindowInsets.safeDrawingfor interactive content - Test gesture nav bar + display cutout on physical device
Composable Contract
kotlin@Composable fun AccountCard( account: AccountUi, onClick: () -> Unit, modifier: Modifier = Modifier // always first param after required data ) { Card( modifier = modifier .fillMaxWidth() .clip(RoundedCornerShape(12.dp)) .clickable(onClick = onClick) ) { Text( text = stringResource(R.string.account_title, account.name), style = MaterialTheme.typography.titleMedium ) } }
Recomposition Rules
- Extract sub-composables for separate restart scopes
- Defer scroll/animation reads:
Modifier.offset { IntOffset(...) } @Immutableon UiState / list wrappers passed to childrenremember(keys) { derivedStateOf { } }for expensive derived values- Never write state in composable body after read (backwards write)
LazyColumn Checklist
-
key = { it.id }on everyitems() -
contentTypewhen row layouts differ - Stable lambdas:
remember(id) { { onClick(id) } } - No nested
verticalScroll+LazyColumn - Sort/filter in ViewModel or
remember, not inline initems {}
Anti-Patterns (Compose)
| Banned | Fix |
|---|---|
Hardcoded Text("Save") | stringResource(R.string.action_save) |
collectAsState() on Android | collectAsStateWithLifecycle() |
Modifier.clickable before padding when touch target should exclude margin | Order: padding → clip → background → clickable |
items(list) without key | items(list, key = { it.id }) |
Raw fontSize = 16.sp | MaterialTheme.typography.bodyLarge |
Full list: ../../references/00-banned-antipatterns.md


