# Android Compose

> Jetpack Compose UI patterns for Android: modifiers, lists, TextFields, Scaffold, previews, performance, navigation with Compose, dialogs, focus, LaunchedEffect and one-shot events, and light animation. Use when building Compose screens, fixing effect timing, list/input polish, or motion. Complements android-interview-bootstrap state hygiene.

- Skill: `kathleenmaas/android-compose` (Agent Skill)
- Install (CLI): `npx skillmds@latest add kathleenmaas/android-compose`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kathleenmaas/android-compose/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: KathleenMaas (https://skillmd.com/u/kathleenmaas)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/kathleenmaas/android-compose

---


# Android Compose

Bootstrap skill covers **structure + UiState + lifecycle collect**. This skill
covers **Compose UI patterns, effects, and light motion**.

## State reminder (don’t duplicate bootstrap)

- Screen state in ViewModel `UiState`; Composables = state + event lambdas
- `Route` (VM) → stateless `Screen` (previewable)
- Collect with `collectAsStateWithLifecycle()`

---

## Modifiers

Common chain (order matters — applied top to bottom / outside-in for layout):

```kotlin
Modifier
    .fillMaxWidth()
    .padding(16.dp)
    .clip(RoundedCornerShape(12.dp))
    .background(color)
    .clickable(onClick = onClick)
    .padding(12.dp) // inner padding after clickable = larger touch target
```

| Need | Modifier |
|------|----------|
| Fill / fraction | `fillMaxSize()`, `fillMaxWidth()`, `fillMaxHeight(0.5f)` |
| Row/Column flex | `Modifier.weight(1f)` (**only** inside Row/Column scope) |
| Click | `clickable` / `combinedClickable`; prefer on row, not only on Text |
| Keyboard insets | `imePadding()` on scroll/content near fields |
| System bars | `systemBarsPadding()` / `navigationBarsPadding()` as needed |
| Clip + ripple | `clip(shape)` then `clickable` (or Material surface) |

`Modifier.weight` only exists in `RowScope`/`ColumnScope` — it won’t compile
elsewhere, so don’t design layouts assuming it.

---

## Lists

```kotlin
LazyColumn(
    contentPadding = PaddingValues(16.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp),
) {
    items(items, key = { it.id }, contentType = { "item" }) { item ->
        ItemRow(item, onClick = { onItemClick(item.id) })
    }
}
```

- Always pass stable **`key`**
- `contentType` when row shapes differ (helps recycling)
- Avoid nested `LazyColumn` inside another vertical scroll without nested scroll fix
- Sticky headers only if the prompt needs them (`stickyHeader { }`)
- Paging: `collectAsLazyPagingItems()` — see bootstrap skill

---

## TextFields

Controlled from state (no uncontrolled field state in production screens):

```kotlin
OutlinedTextField(
    value = state.query,
    onValueChange = onQueryChange,
    singleLine = true,
    keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
    keyboardActions = KeyboardActions(onSearch = { onSubmit(); focusManager.clearFocus() }),
)
```

- Keep value in `UiState`; VM validates if needed (`emailError` separate from screen `error`)
- Use `KeyboardOptions` / `ImeAction` for Done/Search/Next
- Hide keyboard on submit / navigate (`LocalFocusManager` / `LocalSoftwareKeyboardController`)

---

## Scaffold

```kotlin
Scaffold(
    topBar = { TopAppBar(title = { Text(title) }) },
    snackbarHost = { SnackbarHost(snackbarHostState) },
    floatingActionButton = { /* optional */ },
) { innerPadding ->
    Content(modifier = Modifier.padding(innerPadding))
}
```

- Always apply `innerPadding` so content isn’t under the app bar
- Snackbar host + one-shot effects (below) for ephemeral messages
- Scroll-aware top bars only if polish time remains

---

## Previews

```kotlin
@Preview(showBackground = true)
@Composable
private fun HomeScreenPreview() {
    AppTheme {
        HomeScreen(
            state = HomeUiState(items = listOf(/* fake */)),
            onRetry = {},
            onItemClick = {},
        )
    }
}
```

- Preview the **stateless `Screen`**, not the Hilt `Route`
- Multiple previews for loading / empty / error / content when those states matter

---

## Performance (practical)

- Prefer **immutable** `UiState` data classes; replace whole state with `copy`
- Don’t allocate heavy objects or read disk/network during composition
- Hoist stable lambdas from list rows when easy (`onItemClick` from parent)
- Use `key { }` in Lazy lists
- Skip Compose Compiler stability deep-dives / `@Stable` annotations unless
  profiling shows a real issue
- `derivedStateOf` when deriving expensive values from frequently changing state

---

## Navigation + Compose

- Keep `NavController` in the Composition (Activity / NavHost level)
- Pass `onNavigateToDetail: (String) -> Unit` into screens — **don’t** put
  `NavController` in the ViewModel
- Prefer typed routes (Navigation 2.8+) for arguments; add the Kotlin
  serialization plugin
- Restore state flags only if multi-back-stack tabs appear

```kotlin
@Serializable data class Detail(val id: String)

composable<Detail> { entry ->
    val route = entry.toRoute<Detail>()
    DetailRoute(id = route.id, onBack = { navController.popBackStack() })
}
```

---

## Dialogs / sheets

Drive from **state**, not fire-and-forget:

```kotlin
if (state.showDialog) {
    AlertDialog(
        onDismissRequest = onDismissDialog,
        title = { Text("Confirm") },
        text = { Text(state.dialogMessage) },
        confirmButton = { TextButton(onClick = onConfirm) { Text("OK") } },
        dismissButton = { TextButton(onClick = onDismissDialog) { Text("Cancel") } },
    )
}
```

- `showDialog: Boolean` (or sealed dialog type) on `UiState`
- Modal bottom sheet: same idea — visibility from state; clear on dismiss
- Don’t spawn dialogs only inside click handlers without state. ViewModel state
  handles configuration changes; use `SavedStateHandle` if it must survive
  process recreation.

---

## Focus / keyboard

```kotlin
val focusManager = LocalFocusManager.current
val keyboard = LocalSoftwareKeyboardController.current

fun dismissInput() {
    focusManager.clearFocus()
    keyboard?.hide()
}
```

- `FocusRequester` + `Modifier.focusRequester` + `LaunchedEffect` to focus on open
  when the prompt wants autofocus
- Clear focus when navigating away or submitting

---

## Effect APIs

| API | Use for |
|-----|---------|
| `LaunchedEffect(key)` | Suspend side effects tied to composition (collect Channel, debounce) |
| `DisposableEffect(key)` | Listeners; must `onDispose { }` |
| `rememberCoroutineScope()` | Launch from **callbacks** (clicks) |
| `SideEffect` | Rare — sync to non-Compose code |

### Rules

- Keys = values that should **restart** the effect when changed
- Don’t use `LaunchedEffect(Unit)` for VM work (`refresh()` → VM `init` / event)
- Prefer `StateFlow` in VM over long-lived collectors in Composables
- Ephemeral UI (Snackbar): VM `Channel` / `SharedFlow` → UI collects once

```kotlin
// VM
private val _effects = Channel<UiEffect>(Channel.BUFFERED)
val effects = _effects.receiveAsFlow()

// UI
LaunchedEffect(Unit) {
    effects.collect { effect ->
        when (effect) {
            is UiEffect.Snackbar -> snackbarHostState.showSnackbar(effect.message)
        }
    }
}
```

Sticky errors stay on `UiState.error`; effects are for **one-shot** polish.

---

## Animations (basic)

Readability > spectacle. Use lightly in interviews.

| API | Use |
|-----|-----|
| `AnimatedVisibility` | Show/hide blocks (error banner, extra fields) |
| `animate*AsState` | Simple color/size/alpha toward a target |
| `Crossfade(targetState)` | Switch loading ↔ content |
| `Modifier.animateItem()` | Lazy list reorder/item changes |

```kotlin
AnimatedVisibility(visible = state.error != null) {
    // Exit animation may still compose after error becomes null; never use !!
    Text(state.error.orEmpty(), color = MaterialTheme.colorScheme.error)
}

val alpha by animateFloatAsState(if (state.isLoading) 0.5f else 1f, label = "alpha")
```

Always pass `label` for animate* APIs (Studio tooling).

**Skip unless asked:** custom `Transition` graphs, shared element, complex
gesture-driven animation.

---

## Related

- App structure / MVVM / Room / Paging:
  [../android-interview-bootstrap/SKILL.md](../android-interview-bootstrap/SKILL.md)

