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+)
class 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 every ComponentActivity
- Respect
WindowInsets — never hardcode status bar padding
- Use
WindowInsets.safeDrawing for interactive content
- Test gesture nav bar + display cutout on physical device
Composable Contract
@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(...) }
@Immutable on UiState / list wrappers passed to children
remember(keys) { derivedStateOf { } } for expensive derived values
- Never write state in composable body after read (backwards write)
LazyColumn Checklist
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
1---2name: android-kotlin-compose3description: Jetpack Compose UI engineering for 2026 — edge-to-edge, Material 3, recomposition stability, Modifier ordering, LazyColumn performance, and animations. Use when building composables, fixing jank, theming, adaptive layouts, Canvas drawing, auditing screens, or asking "why does my screen recompose", "Modifier order", "edge to edge Compose", "audit this composable", "review this screen".4---56# Android Kotlin — Compose UI Module78Parent kit: [`../../SKILL.md`](../../SKILL.md) · Index: [`../../AGENTS.md`](../../AGENTS.md) · Banned list: [`../../references/00-banned-antipatterns.md`](../../references/00-banned-antipatterns.md)910## REVIEW MODE1112**Trigger phrases:** "audit this file", "check this composable", "review this screen", "review mode", "a11y pass on this composable".1314When 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`](../../references/00-banned-antipatterns.md).1516| # | Check | Fail if |17|---|-------|---------|18| 1 | **Modifier param** | Missing `modifier: Modifier = Modifier` or not last optional param |19| 2 | **State hoisting** | Screen/business state inside `@Composable` that belongs in ViewModel |20| 3 | **Lazy list keys** | `LazyColumn`/`LazyRow` items missing `key = {}` or `contentType = {}` |21| 4 | **Lifecycle collection** | Uses `collectAsState()` instead of `collectAsStateWithLifecycle()` |22| 5 | **Side effects in composition** | IO, DB, network, or `viewModelScope` work directly in composable body |23| 6 | **Stability** | Data class params to child composables lack `@Stable` or `@Immutable` |2425**Output format for REVIEW MODE:**2627```28## Compose audit — <filename>2930| # | Status | Finding |31|---|--------|---------|32| 1 | PASS/FAIL | ... |33...3435### Banned antipatterns hit36- Row N: <pattern> → <fix>3738### Suggested diff39<code block with minimal fix>40```4142Load [`../../references/17-accessibility.md`](../../references/17-accessibility.md) when audit includes TalkBack, touch targets, or contrast.4344---4546## AUTHORING MODE4748Default behavior when **writing new Compose UI** (not auditing). Follow all rules below.4950## Read First5152| File | When |53|------|------|54| [`../../references/02-compose-ui.md`](../../references/02-compose-ui.md) | Stability, slots, Material 3, CompositionLocal themes |55| [`../../references/03-animations.md`](../../references/03-animations.md) | Springs, Canvas, gestures |56| [`../../references/10-performance.md`](../../references/10-performance.md) | derivedStateOf, LazyColumn keys, baseline profiles |57| [`../../references/16-coil-image.md`](../../references/16-coil-image.md) | AsyncImage, Coil 3.4 network dep |58| [`../../references/17-accessibility.md`](../../references/17-accessibility.md) | Semantics, 48dp targets, WCAG |5960## Edge-to-Edge (Default — AGP 9 / API 35+)6162```kotlin63class MainActivity : ComponentActivity() {64 override fun onCreate(savedInstanceState: Bundle?) {65 enableEdgeToEdge()66 super.onCreate(savedInstanceState)67 setContent {68 AppTheme {69 Scaffold(70 modifier = Modifier.fillMaxSize(),71 contentWindowInsets = WindowInsets(0, 0, 0, 0)72 ) { innerPadding ->73 NavHost(74 modifier = Modifier75 .fillMaxSize()76 .padding(innerPadding)77 )78 }79 }80 }81 }82}83```8485- `enableEdgeToEdge()` on every `ComponentActivity`86- Respect `WindowInsets` — never hardcode status bar padding87- Use `WindowInsets.safeDrawing` for interactive content88- Test gesture nav bar + display cutout on physical device8990## Composable Contract9192```kotlin93@Composable94fun AccountCard(95 account: AccountUi,96 onClick: () -> Unit,97 modifier: Modifier = Modifier // always first param after required data98) {99 Card(100 modifier = modifier101 .fillMaxWidth()102 .clip(RoundedCornerShape(12.dp))103 .clickable(onClick = onClick)104 ) {105 Text(106 text = stringResource(R.string.account_title, account.name),107 style = MaterialTheme.typography.titleMedium108 )109 }110}111```112113## Recomposition Rules1141151. Extract sub-composables for separate restart scopes1162. Defer scroll/animation reads: `Modifier.offset { IntOffset(...) }`1173. `@Immutable` on UiState / list wrappers passed to children1184. `remember(keys) { derivedStateOf { } }` for expensive derived values1195. Never write state in composable body after read (backwards write)120121## LazyColumn Checklist122123- [ ] `key = { it.id }` on every `items()`124- [ ] `contentType` when row layouts differ125- [ ] Stable lambdas: `remember(id) { { onClick(id) } }`126- [ ] No nested `verticalScroll` + `LazyColumn`127- [ ] Sort/filter in ViewModel or `remember`, not inline in `items {}`128129## Anti-Patterns (Compose)130131| Banned | Fix |132|--------|-----|133| Hardcoded `Text("Save")` | `stringResource(R.string.action_save)` |134| `collectAsState()` on Android | `collectAsStateWithLifecycle()` |135| `Modifier.clickable` before `padding` when touch target should exclude margin | Order: padding → clip → background → clickable |136| `items(list)` without key | `items(list, key = { it.id })` |137| Raw `fontSize = 16.sp` | `MaterialTheme.typography.bodyLarge` |138139Full list: [`../../references/00-banned-antipatterns.md`](../../references/00-banned-antipatterns.md)