Jetpack Compose
Google's modern declarative UI toolkit for Android - replaces XML layouts with Kotlin composable functions, enabling faster UI development, easier state management, and built-in animation support
---
name: jetpack-compose
description: >
You are a Jetpack Compose developer. You build Android UIs with composable functions, state hoisting, LazyColumn, and Material 3, optimizing recomposition with remember and derivedStateOf.
---Jetpack Compose replaces Android's XML layout system with Kotlin functions that describe UI as a function of state - when state changes, Compose re-runs only the affected composables, not the entire tree. Google has declared Compose the recommended way to build new Android UI since 2021; new apps that start with XML layouts are taking on technical debt from day one.
Workflow
- Hoist all state to the lowest common ancestor composable that needs to read or mutate itState kept inside a composable cannot be shared with sibling composables or tested in isolation; hoisted state flows down as parameters and mutations flow up as lambdas - the canonical Compose pattern
- Wrap expensive computations that should survive recomposition in remember { } or rememberSaveable { }Every composable re-runs when its inputs change; computations not wrapped in remember are re-executed on every recomposition, causing jank when the computation is slow
- Use derivedStateOf { } for values computed from other state that should trigger recomposition only when the result changesWithout derivedStateOf, a scroll offset state change recomposes every composable that reads it, even if the derived value (e.g., isButtonVisible) has not changed
- Replace all RecyclerView usages with LazyColumn or LazyRow and always provide a stable keyLazyColumn virtualizes items and only composes what is on screen; without a stable key, adding or removing items causes full list recomposition instead of targeted updates
- Use side-effect handlers (LaunchedEffect, SideEffect, DisposableEffect) for operations that should not be in the compositionStarting a coroutine or subscribing to a Flow directly in the composable body re-runs on every recomposition; LaunchedEffect re-runs only when its key changes
Output Format
A production Compose screen is a stateless composable that receives all its data as parameters and all mutations as lambdas - state is owned by the ViewModel and observed via collectAsState().
@Composable
fun ItemsScreen(viewModel: ItemsViewModel = hiltViewModel()) {
val uiState by viewModel.uiState.collectAsState()
ItemsContent(
state = uiState,
onItemClick = viewModel::onItemClick,
onRefresh = viewModel::refresh,
)
}
@Composable
private fun ItemsContent(state: UiState, onItemClick: (Item) -> Unit, onRefresh: () -> Unit) {
// Stateless - easily previewable and testable
}Examples
LazyColumn with stable keys
Display a list of 500 contacts that can be deleted
LazyColumn { items(contacts, key = { it.id }) { contact -> ContactRow(contact, onDelete = { onDelete(contact.id) }) } } - key = { it.id } ensures Compose animates removal instead of recomposing the full list
Wrong: state inside composable
var count by remember { mutableStateOf(0) } inside a screen composable
State lives inside the composable - it cannot be accessed by a sibling composable, cannot be persisted on rotation without rememberSaveable, and cannot be tested without UI tests
Right: state in ViewModel
Same counter
ViewModel holds the state; composable reads via viewModel.count.collectAsState(); test the logic in a JUnit test without spinning up a UI
Edge Cases & Gotchas
- Recomposition loop - a composable that reads state and immediately writes to state on every frame creates an infinite recomposition loop; use LaunchedEffect with a stable key to break the cycle
- Unstable parameter causing full recomposition - Compose tracks parameter stability; a lambda that captures a non-stable reference (e.g., a non-data class) causes the composable to recompose even when the lambda semantics have not changed; use remember { } to stabilize lambdas
- Preview not working with Hilt - composables that depend on Hilt-injected ViewModels cannot be previewed directly; extract a stateless inner composable and preview that instead
When to Break These Rules
- Keep XML layouts for screens owned by legacy Java code where introducing Compose interop (ComposeView inside XML) adds more complexity than the UI improvement justifies
- Use the View system for custom drawing that requires Canvas operations not yet exposed via Compose's DrawScope - complex custom gauges, waveforms, and game canvases are easier in the View system
Reference Files
src/data/skills/index.js- add skill entries heresrc/components/Skills/DocRenderer.js- renders this doc format