Jetpack Compose
Jetpack Compose declarative UI toolkit for building native Android interfaces with Kotlin
You are an expert in Jetpack Compose for building Android applications with Kotlin. ## Key Points - Always pass a `Modifier` parameter to public composables and apply it to the root element. - Use `remember` with `derivedStateOf` for expensive computations that depend on state. - Keep composables small and focused; extract sub-composables aggressively. - Use `key` in `LazyColumn` items to preserve state across recompositions. - Prefer `rememberSaveable` over `remember` for state that should survive configuration changes. - Use the `Modifier` chain to express layout, sizing, padding, and interaction — avoid nested wrappers. - Collect `Flow` values with `collectAsStateWithLifecycle()` from the `lifecycle-runtime-compose` library. - Performing heavy computation inside a composable without `remember` or `derivedStateOf`, causing work on every recomposition. - Using `mutableStateListOf` or `mutableStateMapOf` incorrectly — mutations must happen on the snapshot-aware collections directly, not copies. - Forgetting `key` in `LazyColumn`, leading to incorrect item state after list changes. - Nesting scrollable containers (`LazyColumn` inside a `Column` with `verticalScroll`) without using `Modifier.nestedScroll` or fixed-height constraints. - Recomposition skipping: Compose skips recomposition for stable, unchanged parameters. Passing unstable lambdas or objects defeats this optimization.
skilldb get android-kotlin-skills/Jetpack ComposeFull skill: 232 linesJetpack Compose — Android/Kotlin
You are an expert in Jetpack Compose for building Android applications with Kotlin.
Core Philosophy
Jetpack Compose replaces the imperative "mutate the UI to match the new state" model with a declarative one: describe what the UI should look like for a given state, and the framework handles the transitions. This means you stop thinking about "when the user taps this button, update that text field" and start thinking about "the screen is a function of this state object." The framework calls your composable function, diffs the output against the previous result, and updates only what changed. Your job is to get the state right; Compose handles the rest.
Recomposition is the mechanism that makes this work, and understanding it is essential. When state changes, Compose re-executes the composable functions that read that state. It can skip functions whose inputs have not changed, which is why stable and immutable types matter -- they enable the compiler to prove that recomposition can be skipped. Writing composables that perform expensive work without remember or derivedStateOf means that work runs on every recomposition, potentially dozens of times per second during animations.
State hoisting is the foundational pattern for reusable composables. A composable that manages its own state internally cannot be controlled from outside, cannot be tested without rendering, and cannot be reused in different contexts. By lifting state up to the caller and accepting it as parameters along with event callbacks, composables become stateless, reusable building blocks that can be composed in any context.
Anti-Patterns
-
Performing heavy computation inside composable functions without remember: Sorting a list, filtering a collection, or formatting data directly in a composable's body runs on every recomposition. Use
rememberwithderivedStateOfto cache computed values and recalculate only when their dependencies change. -
Forgetting key in LazyColumn items: Without a stable
key, Compose cannot track which items moved, were added, or were removed. This leads to incorrect state being shown on the wrong items (e.g., checkboxes toggling on the wrong row) and defeats Compose's ability to efficiently animate list changes. -
Using mutableStateOf in the wrong scope: Creating
remember { mutableStateOf(...) }inside a child composable that gets recreated by its parent means the state resets whenever the parent recomposes. State must be hoisted to the scope that matches its desired lifetime. -
Nesting scrollable containers without constraints: Placing a
LazyColumninside aColumnwithverticalScrollcreates a conflict where both containers try to consume scroll gestures, usually resulting in a crash. Either give the innerLazyColumna fixed height or usenestedScrollto coordinate. -
Passing unstable lambdas that defeat recomposition skipping: Compose skips recomposing a composable when its parameters have not changed. But if you pass a lambda created inline (like
onClick = { viewModel.doSomething() }) withoutremember, the lambda reference changes on every recomposition, defeating skip optimization. Userememberfor lambdas or reference stable method references.
Overview
Jetpack Compose is Android's modern, fully declarative UI toolkit. Instead of XML layouts, you describe your UI as composable functions that emit UI elements. Compose handles rendering, recomposition, and state management automatically, enabling a reactive programming model for Android UIs.
Core Concepts
Composable Functions
Every UI element in Compose is a @Composable function. These functions describe what the UI should look like for a given state, not how to transition between states.
@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
Text(
text = "Hello, $name!",
modifier = modifier.padding(16.dp),
style = MaterialTheme.typography.headlineMedium
)
}
State and Recomposition
Compose re-executes composable functions when their inputs (state) change. Use remember and mutableStateOf to hold and observe state.
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text("Count: $count", style = MaterialTheme.typography.titleLarge)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
State Hoisting
Lift state up to the caller so composables remain stateless and reusable.
@Composable
fun EmailInput(
email: String,
onEmailChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
OutlinedTextField(
value = email,
onValueChange = onEmailChange,
label = { Text("Email") },
modifier = modifier.fillMaxWidth()
)
}
@Composable
fun LoginScreen() {
var email by remember { mutableStateOf("") }
EmailInput(email = email, onEmailChange = { email = it })
}
Layouts
Compose provides Row, Column, Box, and LazyColumn/LazyRow for arranging elements.
@Composable
fun UserCard(user: User) {
Card(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
AsyncImage(
model = user.avatarUrl,
contentDescription = "Avatar",
modifier = Modifier
.size(48.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(12.dp))
Column {
Text(user.name, style = MaterialTheme.typography.titleMedium)
Text(user.email, style = MaterialTheme.typography.bodySmall)
}
}
}
}
Lazy Lists
LazyColumn and LazyRow only compose visible items, making them efficient for long lists.
@Composable
fun UserList(users: List<User>) {
LazyColumn(
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(users, key = { it.id }) { user ->
UserCard(user = user)
}
}
}
Implementation Patterns
Theming with Material 3
@Composable
fun AppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colorScheme = if (darkTheme) {
dynamicDarkColorScheme(LocalContext.current)
} else {
dynamicLightColorScheme(LocalContext.current)
}
MaterialTheme(
colorScheme = colorScheme,
typography = AppTypography,
content = content
)
}
Side Effects
Use LaunchedEffect, DisposableEffect, and SideEffect to run effects tied to the composable lifecycle.
@Composable
fun TimerScreen() {
var elapsed by remember { mutableStateOf(0) }
LaunchedEffect(Unit) {
while (true) {
delay(1000)
elapsed++
}
}
Text("Elapsed: ${elapsed}s")
}
Animations
@Composable
fun ExpandableCard(title: String, body: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState(
targetValue = if (expanded) 24.dp else 0.dp,
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
)
Card(modifier = Modifier.clickable { expanded = !expanded }) {
Column(modifier = Modifier.padding(16.dp + extraPadding)) {
Text(title, style = MaterialTheme.typography.titleMedium)
AnimatedVisibility(visible = expanded) {
Text(body, modifier = Modifier.padding(top = 8.dp))
}
}
}
}
Best Practices
- Always pass a
Modifierparameter to public composables and apply it to the root element. - Use
rememberwithderivedStateOffor expensive computations that depend on state. - Keep composables small and focused; extract sub-composables aggressively.
- Use
keyinLazyColumnitems to preserve state across recompositions. - Prefer
rememberSaveableoverrememberfor state that should survive configuration changes. - Use the
Modifierchain to express layout, sizing, padding, and interaction — avoid nested wrappers. - Collect
Flowvalues withcollectAsStateWithLifecycle()from thelifecycle-runtime-composelibrary.
Common Pitfalls
- Performing heavy computation inside a composable without
rememberorderivedStateOf, causing work on every recomposition. - Using
mutableStateListOformutableStateMapOfincorrectly — mutations must happen on the snapshot-aware collections directly, not copies. - Forgetting
keyinLazyColumn, leading to incorrect item state after list changes. - Nesting scrollable containers (
LazyColumninside aColumnwithverticalScroll) without usingModifier.nestedScrollor fixed-height constraints. - Recomposition skipping: Compose skips recomposition for stable, unchanged parameters. Passing unstable lambdas or objects defeats this optimization.
- Using
remember { mutableStateOf(...) }in the wrong scope, causing state to reset when parent recomposes.
Install this skill directly: skilldb add android-kotlin-skills
Related Skills
Architecture
MVVM architecture pattern with ViewModel, LiveData, and StateFlow for scalable Android apps
Coroutines
Kotlin coroutines and Flow for asynchronous programming and reactive streams in Android
Dependency Injection
Hilt and Dagger dependency injection for managing object creation and scoping in Android apps
Navigation
Jetpack Navigation component for type-safe in-app navigation and deep linking in Android
Retrofit
Retrofit HTTP client for type-safe REST API communication in Android with Kotlin coroutines
Room Database
Room persistence library for local SQLite database access with compile-time query verification in Android