Tutorial: Learn Jetpack Compose from Scratch (2026)
Jetpack Compose is Android's modern UI toolkit that replaces the traditional XML-based layout system. I remember spending hours wrestling with nested ConstraintLayouts and RecyclerView adapters. Compose eliminates all of that by letting you define UI in Kotlin code using composable functions. The UI reacts to state changes automatically, re-composing only the parts that need updating. Compose integrates with the rest of AndroidX, including Navigation, ViewModel, and Material 3, making it a complete framework for building Android apps. This tutorial covers everything you need to go from zero to a production-ready Compose app in 2026.
Compose uses a unidirectional data flow pattern. State flows down from ViewModels to composables via StateFlow, and events flow up from composables to ViewModels via lambda callbacks. The layout system uses Column, Row, Box, and LazyColumn for lists. Modifiers chain together to configure sizing, padding, click handling, and animations. Material 3 theming is built in with dynamic color support on Android 12+. The learning curve is gentler than the old View system because there are fewer moving parts: no XML, no adapters, no fragment transactions for simple screens.
Setting Up a Compose Project
Open Android Studio, select New Project, and choose Empty Activity (Compose). This creates a project with the Compose compiler plugin configured in build.gradle.kts. The minimum SDK should be 26 for broad device coverage, but Compose supports back to API 21. The build.gradle file includes the Compose BOM (Bill of Materials) to manage version alignment, along with Material 3, Navigation Compose, and Lifecycle ViewModel Compose dependencies. Android Studio 2024+ includes a Compose preview panel that renders composables in real-time as you edit. The main activity extends ComponentActivity and calls setContent with your composable root.
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { MyAppTheme { MyApp() } }
}
}
Composable Functions and Modifiers
A composable function is annotated with @Composable and describes a piece of UI. Compose does not return a View object, it emits UI into the composition. Modifiers are the primary way to configure composables. They are chainable, type-safe, and order-dependent. Common modifiers include .fillMaxWidth(), .padding(16.dp), .clickable { }, .background(color), .size(48.dp), and .clip(CircleShape). The order matters because each modifier wraps the previous one. A .padding().clickable() is different from .clickable().padding() because the hit area changes. The Designer page in Android Studio helps visualize modifier chains.
@Composable
fun Greeting(name: String) {
Column(modifier = Modifier.padding(16.dp)) {
Text("Hello $name", style = MaterialTheme.typography.headlineMedium)
Spacer(modifier = Modifier.height(8.dp))
Button(onClick = { /* do something */ }) { Text("Click") }
}
}
State Management in Compose
State in Compose is managed with mutableStateOf, remember, and StateFlow. mutableStateOf creates an observable state holder; Compose automatically re-composes any composable that reads it. remember scopes the state to the composable's lifecycle, surviving recompositions but not configuration changes. For screen-level state, ViewModel exposes StateFlow that composables collect as state using collectAsState(). The derivedStateOf function computes derived state only when its inputs change, avoiding unnecessary recompositions. snapshotFlow bridges Compose state and Kotlin Flow, letting you react to state changes in effects.
var count by remember { mutableStateOf(0) }
val isEven by remember { derivedStateOf { count % 2 == 0 } }
// ViewModel side
val uiState by viewModel.uiState.collectAsState()
Navigation Compose and Screen Routing
Navigation Compose manages screen transitions within a single-activity architecture. Define a NavHost with a start destination and composable routes. Arguments are typed and can be required or optional. The navController manages the back stack and supports deep linking. For complex navigation patterns, use nested navigation graphs to organize features. Pass events from screens to the NavHost level via sealed class callbacks, enabling the host to navigate or show dialogs. The animated navigation add-on provides transition animations between destinations using AnimatedContent or fade-through-slide patterns.
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(onNavigateToDetail = { id -> navController.navigate("detail/$id") }) }
composable("detail/{id}", arguments = listOf(navArgument("id") { type = NavType.IntType })) { backStackEntry -> DetailScreen(id = backStackEntry.arguments?.getInt("id") ?: 0) }
}
Lists, LazyLayouts, and Performance
For scrollable lists, use LazyColumn and LazyRow, which only compose visible items. Each item must have a stable key for efficient recomposition. LazyVerticalGrid and LazyHorizontalGrid display items in a grid layout. For optimal scrolling performance, avoid complex modifier chains inside item lambdas, use remember with stable keys, and avoid rearranging items unless necessary. The LazyLayoutStableID lint checks help catch missing keys. Content padding with contentPadding and arrangement with verticalArrangement/spacedBy customize the list layout. Pull-to-refresh is built into Material 3 with the PullToRefreshBox component.
LazyColumn(verticalArrangement = Arrangement.spacedBy(8.dp)) {
items(items, key = { it.id }) { item ->
Card(modifier = Modifier.fillMaxWidth().clickable { onItemClick(item) }) { Text(item.name) }
}
}
Material 3, Theming, and Dynamic Color
Material 3 (Material You) is the default design system in modern Compose. Define a color scheme with lightColorScheme and darkColorScheme, and pass it to MaterialTheme. Dynamic color on Android 12+ extracts primary, secondary, and tertiary colors from the user's wallpaper. On older devices, fall back to a custom palette. Typography uses the Typography class with TextStyle definitions for display, headline, title, body, and label roles. Shape schemes define rounded corners for small, medium, and large components. Components like NavigationBar, TopAppBar, Card, FloatingActionButton, and BottomSheet automatically adapt.
MaterialTheme(
colorScheme = if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context),
typography = Typography(bodyLarge = TextStyle(fontFamily = FontFamily.Default)),
shapes = Shapes(small = RoundedCornerShape(8.dp))
) { Surface { MyApp() } }
Frequently Asked Questions
Do I need to learn XML layouts before Compose?
No. Compose is a completely new system. You can start with Compose directly. The concepts of layout, state, and event handling transfer, but the implementation is entirely Kotlin-based.
Can I use Compose with an existing Android project?
Yes. Compose can be added incrementally to existing View-based projects. Use ComposeView in XML layouts to embed composables. Migration tools and interop APIs make gradual adoption straightforward.
How does Compose handle configuration changes?
Compose recomposes with the new configuration. State held in ViewModel survives configuration changes. State held with remember survives recomposition but not configuration changes, use rememberSaveable for that.
When should I use LazyColumn vs Column with scroll?
Use LazyColumn for large or infinite lists where you benefit from item recycling. Use Column with verticalScroll for small fixed-content screens. LazyColumn only composes visible items, saving memory.
Originally published on Ayodhyyya. Last updated June 1, 2026.