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
| Composable | Description |
|---|
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
| Modifier | Description |
|---|
.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
)
}
Navigation
// 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
| Issue | Solution |
|---|
| Recomposition not triggered | Ensure state is read inside @Composable function, not in lambda capture |
| Preview not rendering | Add @Preview annotation; check for missing default parameters |
| LazyColumn items not updating | Provide stable key parameter: items(list, key = { it.id }) |
| Performance / janky scrolling | Use remember for expensive calculations; avoid allocations in composition |
| TextField losing focus | Hoist state properly; avoid recreating TextField on recomposition |
| Theme colors not applying | Wrap content in MaterialTheme; use MaterialTheme.colorScheme |
| Navigation crash on back | Check popBackStack() returns boolean; handle empty back stack |
| Keyboard overlapping content | Use imePadding() modifier or WindowInsets.ime |
| State lost on rotation | Use rememberSaveable instead of remember |
| Compose and Views interop issues | Use AndroidView to embed Views; ComposeView to embed Compose in XML |