Jetpack Compose

Introduction
Jetpack Compose is Android's modern toolkit for building native user interfaces. Introduced by Google in 2019, it represents a paradigm shift from the traditional View-based UI system to a declarative approach, similar to React and Flutter.
This comprehensive guide will take you from the basics of Compose to building complex, production-ready UIs. Whether you're new to Android development or an experienced developer transitioning from Views, this guide will help you master Jetpack Compose.
Why Jetpack Compose?
Before diving into the code, let's understand why Compose is the future of Android UI development:
- Declarative UI: Describe what you want, not how to create it
- Less Code: Significantly fewer lines of code compared to XML layouts
- Type Safety: Compile-time safety for UI components
- Better Performance: More efficient rendering and updates
- Kotlin-First: Built specifically for Kotlin
- Live Preview: Real-time preview of your UI changes
- Backward Compatibility: Works with existing View-based code
Setting Up Jetpack Compose
To use Jetpack Compose in your project, you need to add the necessary dependencies and configure your project.
Dependencies
// app/build.gradle.kts
android {
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.1"
}
}
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2023.10.01")
implementation(composeBom)
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.8.2")
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
Minimum Requirements
- Android Studio: Arctic Fox (2020.3.1) or later
- Kotlin: 1.8.10 or later
- Minimum SDK: API 21 (Android 5.0)
- Target SDK: API 34 (Android 14)
Your First Compose App
Let's start with a simple "Hello World" example to understand the basic structure.
Basic Composable Function
@Composable
fun HelloWorld() {
Text(text = "Hello, Compose!")
}
@Composable
fun MyApp() {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
HelloWorld()
}
}
}
Activity Setup
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp()
}
}
}
Understanding the Structure
- @Composable: Annotation that marks a function as a composable
- MaterialTheme: Provides design tokens and theming
- Surface: Material Design container with elevation
- Modifier: Chain of modifications for layout and behavior
Basic Composables
Compose provides a rich set of built-in composables for common UI elements.
Text Composable
@Composable
fun TextExamples() {
Column {
// Basic text
Text("Simple text")
// Styled text
Text(
text = "Styled text",
color = Color.Blue,
fontSize = 18.sp,
fontWeight = FontWeight.Bold
)
// Text with modifier
Text(
text = "Text with background",
modifier = Modifier
.background(Color.Yellow)
.padding(8.dp)
)
// Annotated text
Text(
text = buildAnnotatedString {
withStyle(SpanStyle(color = Color.Red)) {
append("Red text ")
}
withStyle(SpanStyle(color = Color.Blue)) {
append("Blue text")
}
}
)
}
}
Button Composable
@Composable
fun ButtonExamples() {
Column(spacing = 8.dp) {
// Basic button
Button(onClick = { /* Handle click */ }) {
Text("Click me")
}
// Outlined button
OutlinedButton(onClick = { /* Handle click */ }) {
Text("Outlined Button")
}
// Text button
TextButton(onClick = { /* Handle click */ }) {
Text("Text Button")
}
// Button with icon
Button(
onClick = { /* Handle click */ },
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add icon"
)
Spacer(modifier = Modifier.width(8.dp))
Text("Add Item")
}
}
}
Image Composable
@Composable
fun ImageExamples() {
Column {
// Basic image
Image(
painter = painterResource(id = R.drawable.my_image),
contentDescription = "My image"
)
// Image with modifier
Image(
painter = painterResource(id = R.drawable.my_image),
contentDescription = "My image",
modifier = Modifier
.size(100.dp)
.clip(CircleShape)
)
// Async image with Coil
AsyncImage(
model = "https://example.com/image.jpg",
contentDescription = "Remote image",
modifier = Modifier.size(200.dp),
contentScale = ContentScale.Crop
)
}
}
Layout Composables
Compose provides several layout composables to arrange your UI elements.
Column and Row
@Composable
fun LayoutExamples() {
Column(
modifier = Modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text("Column - Vertical Layout")
Spacer(modifier = Modifier.height(16.dp))
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier.fillMaxWidth()
) {
Text("Row - Horizontal Layout")
Text("Item 2")
Text("Item 3")
}
Spacer(modifier = Modifier.height(16.dp))
// Nested layouts
Column {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
Text("Left")
Text("Right")
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
Text("Evenly")
Text("Spaced")
Text("Items")
}
}
}
}
Box Layout
@Composable
fun BoxExamples() {
Box(
modifier = Modifier
.size(200.dp)
.background(Color.LightGray)
) {
// Background content
Text(
text = "Background",
modifier = Modifier.align(Alignment.Center)
)
// Overlay content
Text(
text = "Overlay",
modifier = Modifier
.align(Alignment.TopStart)
.background(Color.Red.copy(alpha = 0.7f))
.padding(4.dp)
)
// Another overlay
Button(
onClick = { /* Handle click */ },
modifier = Modifier.align(Alignment.BottomEnd)
) {
Text("Button")
}
}
}
LazyColumn and LazyRow
@Composable
fun LazyListExamples() {
val items = List(100) { "Item $it" }
LazyColumn {
items(items) { item ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
.background(Color.White)
)
}
}
}
@Composable
fun LazyRowExample() {
val items = List(20) { "Item $it" }
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(horizontal = 16.dp)
) {
items(items) { item ->
Card(
modifier = Modifier.width(120.dp)
) {
Text(
text = item,
modifier = Modifier.padding(8.dp)
)
}
}
}
}
State Management
State management is crucial in Compose. Understanding how to handle state properly will make your apps more responsive and maintainable.
Remember and MutableState
@Composable
fun CounterExample() {
var count by remember { mutableStateOf(0) }
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
Button(onClick = { count-- }) {
Text("Decrement")
}
}
}
State Hoisting
@Composable
fun StateHoistingExample() {
var text by remember { mutableStateOf("") }
Column {
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Enter text") }
)
Text("You entered: $text")
// Child component receives state as parameter
ChildComponent(
text = text,
onTextChange = { text = it }
)
}
}
@Composable
fun ChildComponent(
text: String,
onTextChange: (String) -> Unit
) {
TextField(
value = text,
onValueChange = onTextChange,
label = { Text("Child input") }
)
}
Derived State
@Composable
fun DerivedStateExample() {
var firstName by remember { mutableStateOf("") }
var lastName by remember { mutableStateOf("") }
// Derived state - computed from other state
val fullName by remember(firstName, lastName) {
derivedStateOf {
if (firstName.isBlank() && lastName.isBlank()) {
"No name entered"
} else {
"$firstName $lastName".trim()
}
}
}
Column {
TextField(
value = firstName,
onValueChange = { firstName = it },
label = { Text("First Name") }
)
TextField(
value = lastName,
onValueChange = { lastName = it },
label = { Text("Last Name") }
)
Text("Full Name: $fullName")
}
}
Material Design 3
Material Design 3 is the latest design system from Google, and Compose makes it easy to implement.
Material 3 Theme
@Composable
fun Material3Example() {
MaterialTheme(
colorScheme = lightColorScheme(
primary = Color(0xFF6750A4),
secondary = Color(0xFF625B71),
tertiary = Color(0xFF7D5260)
),
typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
)
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Material 3 App",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { /* Handle click */ },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Text("Primary Button")
}
Spacer(modifier = Modifier.height(8.dp))
OutlinedButton(
onClick = { /* Handle click */ }
) {
Text("Secondary Button")
}
}
}
}
}
Cards and Elevation
@Composable
fun CardExamples() {
Column(spacing = 16.dp) {
// Elevated card
ElevatedCard(
onClick = { /* Handle click */ },
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Elevated Card",
style = MaterialTheme.typography.headlineSmall
)
Text(
text = "This card has elevation and is clickable",
style = MaterialTheme.typography.bodyMedium
)
}
}
// Filled card
FilledCard(
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Filled Card",
style = MaterialTheme.typography.headlineSmall
)
Text(
text = "This card has a filled background",
style = MaterialTheme.typography.bodyMedium
)
}
}
// Outlined card
OutlinedCard(
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Outlined Card",
style = MaterialTheme.typography.headlineSmall
)
Text(
text = "This card has an outline",
style = MaterialTheme.typography.bodyMedium
)
}
}
}
}
Navigation
Navigation in Compose is handled by the Navigation Compose library, which provides a type-safe way to navigate between screens.
Basic Navigation Setup
// Add to dependencies
implementation("androidx.navigation:navigation-compose:2.7.5")
@Composable
fun NavigationExample() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(navController)
}
composable("profile") {
ProfileScreen(navController)
}
composable("settings") {
SettingsScreen(navController)
}
}
}
@Composable
fun HomeScreen(navController: NavController) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(
text = "Home Screen",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
Button(
onClick = { navController.navigate("profile") }
) {
Text("Go to Profile")
}
Spacer(modifier = Modifier.height(8.dp))
Button(
onClick = { navController.navigate("settings") }
) {
Text("Go to Settings")
}
}
}
Navigation with Arguments
@Composable
fun NavigationWithArgs() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "home"
) {
composable("home") {
HomeScreen(navController)
}
composable(
"detail/{userId}",
arguments = listOf(
navArgument("userId") { type = NavType.StringType }
)
) { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId")
DetailScreen(userId = userId, navController = navController)
}
}
}
@Composable
fun HomeScreen(navController: NavController) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text("Home Screen")
Button(
onClick = { navController.navigate("detail/user123") }
) {
Text("View User Details")
}
}
}
@Composable
fun DetailScreen(userId: String?, navController: NavController) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text("User ID: $userId")
Button(
onClick = { navController.popBackStack() }
) {
Text("Go Back")
}
}
}
Practical Examples
Let's build some real-world examples to demonstrate Compose concepts in action.
Todo List App
data class Todo(
val id: Int,
val text: String,
val isCompleted: Boolean = false
)
@Composable
fun TodoApp() {
var todos by remember { mutableStateOf(listOf()) }
var newTodoText by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// Add new todo
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
TextField(
value = newTodoText,
onValueChange = { newTodoText = it },
modifier = Modifier.weight(1f),
placeholder = { Text("Add a new todo") }
)
Spacer(modifier = Modifier.width(8.dp))
Button(
onClick = {
if (newTodoText.isNotBlank()) {
todos = todos + Todo(
id = todos.size,
text = newTodoText
)
newTodoText = ""
}
}
) {
Text("Add")
}
}
Spacer(modifier = Modifier.height(16.dp))
// Todo list
LazyColumn {
items(todos) { todo ->
TodoItem(
todo = todo,
onToggle = {
todos = todos.map {
if (it.id == todo.id) it.copy(isCompleted = !it.isCompleted)
else it
}
},
onDelete = {
todos = todos.filter { it.id != todo.id }
}
)
}
}
}
}
@Composable
fun TodoItem(
todo: Todo,
onToggle: () -> Unit,
onDelete: () -> Unit
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = todo.isCompleted,
onCheckedChange = { onToggle() }
)
Text(
text = todo.text,
modifier = Modifier.weight(1f),
textDecoration = if (todo.isCompleted) {
TextDecoration.LineThrough
} else {
TextDecoration.None
}
)
IconButton(onClick = onDelete) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = "Delete todo"
)
}
}
}
Weather App UI
data class WeatherInfo(
val temperature: Int,
val condition: String,
val humidity: Int,
val windSpeed: Int
)
@Composable
fun WeatherApp() {
var weatherInfo by remember {
mutableStateOf(WeatherInfo(22, "Sunny", 65, 12))
}
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp)
) {
// Header
Text(
text = "Weather App",
style = MaterialTheme.typography.headlineLarge,
modifier = Modifier.padding(bottom = 16.dp)
)
// Weather card
ElevatedCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column {
Text(
text = "${weatherInfo.temperature}°C",
style = MaterialTheme.typography.headlineLarge
)
Text(
text = weatherInfo.condition,
style = MaterialTheme.typography.bodyLarge
)
}
Icon(
imageVector = Icons.Default.WbSunny,
contentDescription = "Weather icon",
modifier = Modifier.size(48.dp),
tint = Color.Yellow
)
}
Spacer(modifier = Modifier.height(16.dp))
// Weather details
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
WeatherDetail(
icon = Icons.Default.Opacity,
label = "Humidity",
value = "${weatherInfo.humidity}%"
)
WeatherDetail(
icon = Icons.Default.Air,
label = "Wind",
value = "${weatherInfo.windSpeed} km/h"
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// Refresh button
Button(
onClick = {
// Simulate weather update
weatherInfo = weatherInfo.copy(
temperature = (15..30).random(),
humidity = (40..80).random(),
windSpeed = (5..20).random()
)
},
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.Refresh,
contentDescription = "Refresh"
)
Spacer(modifier = Modifier.width(8.dp))
Text("Refresh Weather")
}
}
}
}
}
@Composable
fun WeatherDetail(
icon: ImageVector,
label: String,
value: String
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
imageVector = icon,
contentDescription = label,
modifier = Modifier.size(24.dp)
)
Text(
text = label,
style = MaterialTheme.typography.bodySmall
)
Text(
text = value,
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.Bold
)
}
}
Performance Optimization
Compose is designed to be performant, but there are several techniques you can use to optimize your apps.
Remember and RememberSaveable
@Composable
fun PerformanceExample() {
// Use remember for expensive computations
val expensiveValue by remember {
derivedStateOf {
// Expensive computation
(1..1000).sum()
}
}
// Use rememberSaveable for state that survives configuration changes
var userInput by rememberSaveable { mutableStateOf("") }
Column {
Text("Expensive value: $expensiveValue")
TextField(
value = userInput,
onValueChange = { userInput = it }
)
}
}
Lazy Loading
@Composable
fun LazyLoadingExample() {
val items = List(1000) { "Item $it" }
LazyColumn {
items(
items = items,
key = { it } // Stable keys for better performance
) { item ->
Text(
text = item,
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
)
}
}
}
Composition Local
// Define composition local
val LocalTheme = compositionLocalOf { "light" }
@Composable
fun ThemeProvider() {
CompositionLocalProvider(LocalTheme provides "dark") {
ChildComponent()
}
}
@Composable
fun ChildComponent() {
val theme = LocalTheme.current
Text("Current theme: $theme")
}
Testing
Testing Compose UI is straightforward with the Compose testing library.
Basic UI Tests
// Add to dependencies
androidTestImplementation("androidx.compose.ui:ui-test-junit4:1.5.4")
debugImplementation("androidx.compose.ui:ui-test-manifest:1.5.4")
@Composable
fun TestableComponent() {
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
@Test
fun testIncrementButton() {
composeTestRule.setContent {
TestableComponent()
}
// Find text and verify initial state
composeTestRule.onNodeWithText("Count: 0").assertExists()
// Click button
composeTestRule.onNodeWithText("Increment").performClick()
// Verify state changed
composeTestRule.onNodeWithText("Count: 1").assertExists()
}
Semantics for Testing
@Composable
fun TestableButton() {
Button(
onClick = { /* Handle click */ },
modifier = Modifier.testTag("increment_button")
) {
Text("Increment")
}
}
@Test
fun testButtonWithTag() {
composeTestRule.setContent {
TestableButton()
}
// Find by test tag
composeTestRule.onNodeWithTag("increment_button").performClick()
}
Best Practices
Composable Design
- Keep composables small: Break large composables into smaller, reusable ones
- Use meaningful names: Name your composables descriptively
- Extract parameters: Make composables configurable through parameters
- Follow single responsibility: Each composable should have one clear purpose
State Management
- Hoist state up: Keep state as close to where it's used as possible
- Use remember appropriately: Remember expensive computations and state
- Prefer immutable state: Use data classes for complex state
- Use derived state: Compute values from other state when possible
Performance
- Use LazyColumn/LazyRow: For long lists
- Provide stable keys: For list items and animations
- Minimize recompositions: Use remember and derived state
- Profile your app: Use Android Studio's profiler
Common Pitfalls
Avoiding Common Mistakes
- Don't call composables conditionally: Always call them in the same order
- Don't forget remember: Use remember for state and expensive computations
- Don't ignore side effects: Use LaunchedEffect for one-time effects
- Don't over-compose: Keep composables focused and not too deep
Debugging Tips
- Use Compose Preview: Test your UI in isolation
- Enable recomposition counting: In debug builds
- Use Layout Inspector: To debug layout issues
- Check Compose Compiler: For compilation errors
Practice Exercises
Try these exercises to reinforce your Compose knowledge:
Exercise 1: Counter App
// Create a counter app with:
// - Display current count
// - Increment and decrement buttons
// - Reset button
// - Color-coded display (red for negative, green for positive)
Exercise 2: Form Validation
// Create a form with:
// - Email input with validation
// - Password input with strength indicator
// - Submit button (enabled only when valid)
// - Error messages for invalid inputs
Exercise 3: Shopping Cart
// Create a shopping cart with:
// - List of products
// - Add/remove items
// - Quantity controls
// - Total price calculation
// - Checkout button
Next Steps
Now that you have a solid foundation in Jetpack Compose, explore these advanced topics:
- Animations: Learn animate*AsState and transition APIs
- Custom Composables: Create reusable UI components
- Integration with Views: Mix Compose with existing View code
- Advanced State: Explore StateFlow and ViewModel integration
- Material 3 Theming: Create custom design systems
- Performance Profiling: Optimize your Compose apps
Resources
Summary
Jetpack Compose represents the future of Android UI development. Its declarative approach, type safety, and powerful features make it an excellent choice for building modern Android applications.
You've learned the fundamentals of Compose, from basic composables to state management, navigation, and performance optimization. The key to mastering Compose is practice - build real applications, experiment with different patterns, and stay updated with the latest features and best practices.
Remember that Compose is still evolving, so keep learning and exploring new capabilities. The Android developer community is actively contributing to its growth, and there are always new techniques and patterns to discover.