Mobile Development5 min read

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

AndroidKotlinDeclarative UIGoogleJetpack
---
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

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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

Input

Display a list of 500 contacts that can be deleted

Output

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

Input

var count by remember { mutableStateOf(0) } inside a screen composable

Output

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

Input

Same counter

Output

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 here
  • src/components/Skills/DocRenderer.js - renders this doc format

More skills in this library

Browse all technical skills - Mobile, AI/ML, Web, Backend, Design, and Product.