Skip to content

Jetpack Compose Cheat Sheet

Overview

Jetpack Compose is Android’s recommended modern toolkit for building native UI. It simplifies and accelerates UI development on Android by using a declarative approach where you describe your UI as composable functions in Kotlin. Instead of manipulating views imperatively through XML layouts and view binding, Compose lets you define what the UI should look like for a given state, and the framework handles all the updates when state changes. Compose is fully interoperable with existing Android views, enabling incremental adoption in existing projects.

Compose integrates with the entire Jetpack ecosystem including Navigation, ViewModel, Room, and Hilt. It provides built-in support for Material Design 3, theming, animations, and accessibility. The toolkit generates efficient UI code that performs comparably to the traditional View system while offering significantly improved developer productivity. Compose also extends beyond mobile to Wear OS, Android TV, and desktop through Compose Multiplatform.

Installation

// build.gradle.kts (app level)
plugins {
    id("com.android.application")
    id("org.jetbrains.kotlin.android")
    id("org.jetbrains.kotlin.plugin.compose")
}

android {
    compileSdk = 35
    
    defaultConfig {
        minSdk = 24
        targetSdk = 35
    }
    
    buildFeatures {
        compose = true
    }
}

dependencies {
    // Compose BOM for version alignment
    val composeBom = platform("androidx.compose:compose-bom:2025.01.00")
    implementation(composeBom)
    
    // Core Compose libraries
    implementation("androidx.compose.ui:ui")
    implementation("androidx.compose.ui:ui-graphics")
    implementation("androidx.compose.ui:ui-tooling-preview")
    implementation("androidx.compose.material3:material3")
    implementation("androidx.activity:activity-compose:1.9.3")
    implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.7")
    implementation("androidx.navigation:navigation-compose:2.8.5")
    
    // Debug tooling
    debugImplementation("androidx.compose.ui:ui-tooling")
    debugImplementation("androidx.compose.ui:ui-test-manifest")
    
    // Testing
    androidTestImplementation("androidx.compose.ui:ui-test-junit4")
}
# Create new Compose project via Android Studio
# File > New > New Project > Empty Activity (Compose)

# Or via command line with gradle
./gradlew assembleDebug
./gradlew installDebug

Basic Composables

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Text(
        text = "Hello, $name!",
        modifier = modifier.padding(16.dp),
        style = MaterialTheme.typography.headlineMedium,
        color = MaterialTheme.colorScheme.primary
    )
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    MyAppTheme {
        Greeting("Android")
    }
}

Common Components

ComposableDescription
Text("Hello")Display text
Button(onClick = {}) { Text("Click") }Clickable button
TextField(value, onValueChange)Text input field
Image(painter, contentDescription)Display image
Icon(Icons.Default.Star, "Star")Material icon
Checkbox(checked, onCheckedChange)Boolean checkbox
Switch(checked, onCheckedChange)Toggle switch
RadioButton(selected, onClick)Radio selection
Slider(value, onValueChange)Range slider
CircularProgressIndicator()Loading spinner
LinearProgressIndicator(progress)Progress bar
Card { content }Material card container
Divider()Horizontal divider line
Spacer(modifier = Modifier.height(8.dp))Empty space

Layout

// Column (vertical)
Column(
    modifier = Modifier.fillMaxSize().padding(16.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp),
    horizontalAlignment = Alignment.CenterHorizontally
) {
    Text("First")
    Text("Second")
    Text("Third")
}

// Row (horizontal)
Row(
    modifier = Modifier.fillMaxWidth(),
    horizontalArrangement = Arrangement.SpaceBetween,
    verticalAlignment = Alignment.CenterVertically
) {
    Icon(Icons.Default.Person, "User")
    Text("Username")
    Spacer(modifier = Modifier.weight(1f))
    IconButton(onClick = {}) {
        Icon(Icons.Default.Edit, "Edit")
    }
}

// Box (stacking / overlay)
Box(modifier = Modifier.fillMaxSize()) {
    Image(
        painter = painterResource(R.drawable.background),
        contentDescription = null,
        modifier = Modifier.fillMaxSize(),
        contentScale = ContentScale.Crop
    )
    Text(
        "Overlay Text",
        modifier = Modifier.align(Alignment.Center),
        color = Color.White
    )
}

// LazyColumn (RecyclerView replacement)
LazyColumn(
    contentPadding = PaddingValues(16.dp),
    verticalArrangement = Arrangement.spacedBy(8.dp)
) {
    items(itemsList) { item ->
        ItemCard(item)
    }
}

// LazyVerticalGrid
LazyVerticalGrid(
    columns = GridCells.Fixed(2),
    contentPadding = PaddingValues(8.dp)
) {
    items(products) { product ->
        ProductCard(product)
    }
}

Modifier Chain

ModifierDescription
.padding(16.dp)Add padding
.fillMaxWidth()Fill available width
.fillMaxSize()Fill all available space
.size(48.dp)Set exact size
.weight(1f)Proportional sizing in Row/Column
.background(Color.Blue)Background color
.clip(RoundedCornerShape(8.dp))Clip to shape
.border(1.dp, Color.Gray)Add border
.clickable { }Make clickable
.shadow(4.dp)Add elevation shadow
.alpha(0.5f)Set opacity
.rotate(45f)Rotate element
.verticalScroll(rememberScrollState())Enable scrolling

State Management

// remember + mutableStateOf for local state
@Composable
fun Counter() {
    var count by remember { mutableStateOf(0) }
    
    Column(horizontalAlignment = Alignment.CenterHorizontally) {
        Text("Count: $count", style = MaterialTheme.typography.headlineMedium)
        Button(onClick = { count++ }) {
            Text("Increment")
        }
    }
}

// rememberSaveable survives configuration changes
var text by rememberSaveable { mutableStateOf("") }

// State hoisting pattern
@Composable
fun StatefulCounter() {
    var count by remember { mutableStateOf(0) }
    StatelessCounter(count = count, onIncrement = { count++ })
}

@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit) {
    Column {
        Text("Count: $count")
        Button(onClick = onIncrement) { Text("+1") }
    }
}

// ViewModel integration
class MainViewModel : ViewModel() {
    private val _uiState = MutableStateFlow(UiState())
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    fun updateName(name: String) {
        _uiState.update { it.copy(name = name) }
    }
}

@Composable
fun MainScreen(viewModel: MainViewModel = viewModel()) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    
    TextField(
        value = uiState.name,
        onValueChange = viewModel::updateName
    )
}
// Setup NavHost
@Composable
fun AppNavigation() {
    val navController = rememberNavController()
    
    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(
                onNavigateToDetail = { id ->
                    navController.navigate("detail/$id")
                }
            )
        }
        composable(
            "detail/{itemId}",
            arguments = listOf(navArgument("itemId") { type = NavType.IntType })
        ) { backStackEntry ->
            val itemId = backStackEntry.arguments?.getInt("itemId") ?: 0
            DetailScreen(itemId = itemId)
        }
    }
}

// Bottom navigation
Scaffold(
    bottomBar = {
        NavigationBar {
            NavigationBarItem(
                icon = { Icon(Icons.Default.Home, "Home") },
                label = { Text("Home") },
                selected = currentRoute == "home",
                onClick = { navController.navigate("home") }
            )
            NavigationBarItem(
                icon = { Icon(Icons.Default.Settings, "Settings") },
                label = { Text("Settings") },
                selected = currentRoute == "settings",
                onClick = { navController.navigate("settings") }
            )
        }
    }
) { paddingValues ->
    NavHost(
        modifier = Modifier.padding(paddingValues),
        navController = navController,
        startDestination = "home"
    ) {
        // routes...
    }
}

Theming

// Define color scheme
private val DarkColorScheme = darkColorScheme(
    primary = Purple80,
    secondary = PurpleGrey80,
    tertiary = Pink80
)

private val LightColorScheme = lightColorScheme(
    primary = Purple40,
    secondary = PurpleGrey40,
    tertiary = Pink40
)

@Composable
fun MyAppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) DarkColorScheme else LightColorScheme
    
    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography,
        content = content
    )
}

Animations

// Animate visibility
AnimatedVisibility(
    visible = isVisible,
    enter = fadeIn() + slideInVertically(),
    exit = fadeOut() + slideOutVertically()
) {
    Text("Animated Content")
}

// Animate value changes
val size by animateDpAsState(
    targetValue = if (expanded) 200.dp else 100.dp,
    animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
)

Box(modifier = Modifier.size(size).background(Color.Blue))

// Crossfade between states
Crossfade(targetState = currentScreen) { screen ->
    when (screen) {
        Screen.Home -> HomeScreen()
        Screen.Profile -> ProfileScreen()
    }
}

// Infinite animation
val infiniteTransition = rememberInfiniteTransition()
val alpha by infiniteTransition.animateFloat(
    initialValue = 0.2f,
    targetValue = 1f,
    animationSpec = infiniteRepeatable(
        animation = tween(1000),
        repeatMode = RepeatMode.Reverse
    )
)

Advanced Usage

// Custom layout
@Composable
fun StaggeredGrid(
    modifier: Modifier = Modifier,
    columns: Int = 2,
    content: @Composable () -> Unit
) {
    Layout(content = content, modifier = modifier) { measurables, constraints ->
        val columnWidths = IntArray(columns) { constraints.maxWidth / columns }
        val columnHeights = IntArray(columns) { 0 }
        
        val placeables = measurables.map { measurable ->
            val column = columnHeights.indices.minByOrNull { columnHeights[it] } ?: 0
            val placeable = measurable.measure(
                constraints.copy(maxWidth = columnWidths[column])
            )
            columnHeights[column] += placeable.height
            Triple(placeable, column, columnHeights[column] - placeable.height)
        }
        
        layout(constraints.maxWidth, columnHeights.max()) {
            placeables.forEach { (placeable, column, y) ->
                placeable.placeRelative(columnWidths[0] * column, y)
            }
        }
    }
}

// Side effects
@Composable
fun SideEffectExample() {
    // Run once when entering composition
    LaunchedEffect(Unit) {
        delay(2000)
        // perform async work
    }
    
    // Run on every recomposition
    SideEffect {
        // update non-compose state
    }
    
    // Cleanup when leaving composition
    DisposableEffect(key) {
        val listener = addListener()
        onDispose {
            removeListener(listener)
        }
    }
}

// CompositionLocal for implicit data passing
val LocalAppConfig = compositionLocalOf { AppConfig() }

@Composable
fun AppRoot() {
    CompositionLocalProvider(LocalAppConfig provides AppConfig(darkMode = true)) {
        ChildComposable()
    }
}

Troubleshooting

IssueSolution
Recomposition not triggeredEnsure state is read inside @Composable function, not in lambda capture
Preview not renderingAdd @Preview annotation; check for missing default parameters
LazyColumn items not updatingProvide stable key parameter: items(list, key = { it.id })
Performance / janky scrollingUse remember for expensive calculations; avoid allocations in composition
TextField losing focusHoist state properly; avoid recreating TextField on recomposition
Theme colors not applyingWrap content in MaterialTheme; use MaterialTheme.colorScheme
Navigation crash on backCheck popBackStack() returns boolean; handle empty back stack
Keyboard overlapping contentUse imePadding() modifier or WindowInsets.ime
State lost on rotationUse rememberSaveable instead of remember
Compose and Views interop issuesUse AndroidView to embed Views; ComposeView to embed Compose in XML