Jetpack Compose is Android's modern UI toolkit—new apps using Compose ship 40% faster with 60% less code than Views. Google is all-in on Compose, and the ecosystem is mature. This guide covers everything from Compose basics to advanced patterns used by production apps.
Why Jetpack Compose
Benefits Over Views
- Less code: 60% reduction versus XML + Views
- Declarative: Describe UI state, not how to update it
- Kotlin-first: Full Kotlin power in UI code
- Live previews: See changes instantly
- Material Design 3: Built-in theming
- Interop: Mix with existing Views
- Performance: Optimized by default
When to Use Compose
✓ Use Compose:
- New Android projects
- New features in existing apps
- Rapid UI prototyping
- Modern Material Design
- Kotlin-based projects
✗ Stick with Views:
- Legacy maintenance only
- Heavy MapView usage (improving)
- Extensive custom View investment
- Need Java compatibility
Hybrid approach (recommended):
- New screens in Compose
- Existing screens in Views
- Bridge with AndroidView/ComposeView
- Gradual migration
Getting Started
Setup
// app/build.gradle.kts
android {
compileSdk = 34
defaultConfig {
minSdk = 21
targetSdk = 34
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.8"
}
}
dependencies {
val composeBom = platform("androidx.compose:compose-bom:2024.01.00")
implementation(composeBom)
androidTestImplementation(composeBom)
// Compose dependencies
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")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
implementation("androidx.navigation:navigation-compose:2.7.6")
// Debug
debugImplementation("androidx.compose.ui:ui-tooling")
debugImplementation("androidx.compose.ui:ui-test-manifest")
}
Hello World
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
@Composable
fun Greeting(name: String) {
Text(text = "Hello, $name!")
}
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
Greeting(name = "Compose")
}
// MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyAppTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Greeting(name = "Android")
}
}
}
}
}
Compose Basics
Basic Composables
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
@Composable
fun BasicComposablesDemo() {
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
// Text
Text(
text = "Jetpack Compose",
style = MaterialTheme.typography.headlineLarge,
color = MaterialTheme.colorScheme.primary
)
// Image
Image(
painter = painterResource(R.drawable.sample),
contentDescription = "Sample image",
modifier = Modifier.size(100.dp)
)
// Icon
Icon(
imageVector = Icons.Default.Star,
contentDescription = "Star icon",
tint = Color.Yellow
)
// Button
Button(onClick = { /* Handle click */ }) {
Text("Click Me")
}
// OutlinedButton
OutlinedButton(onClick = { }) {
Text("Outlined")
}
// TextField
var text by remember { mutableStateOf("") }
TextField(
value = text,
onValueChange = { text = it },
label = { Text("Enter text") }
)
// OutlinedTextField
OutlinedTextField(
value = text,
onValueChange = { text = it },
label = { Text("Email") }
)
// Switch
var checked by remember { mutableStateOf(true) }
Switch(
checked = checked,
onCheckedChange = { checked = it }
)
// Checkbox
Checkbox(
checked = checked,
onCheckedChange = { checked = it }
)
// RadioButton
RadioButton(
selected = checked,
onClick = { checked = !checked }
)
// Slider
var sliderValue by remember { mutableStateOf(0.5f) }
Slider(
value = sliderValue,
onValueChange = { sliderValue = it }
)
// CircularProgressIndicator
CircularProgressIndicator()
// LinearProgressIndicator
LinearProgressIndicator(
progress = 0.75f,
modifier = Modifier.fillMaxWidth()
)
}
}
Layouts
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
// Column - Vertical
@Composable
fun ColumnExample() {
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("Item 1")
Text("Item 2")
Text("Item 3")
}
}
// Row - Horizontal
@Composable
fun RowExample() {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text("Left")
Text("Center")
Text("Right")
}
}
// Box - Overlapping
@Composable
fun BoxExample() {
Box(
modifier = Modifier.size(200.dp),
contentAlignment = Alignment.Center
) {
Text("Background", modifier = Modifier.align(Alignment.TopStart))
Text("Foreground")
Text("Bottom", modifier = Modifier.align(Alignment.BottomEnd))
}
}
// LazyColumn - Scrollable list
@Composable
fun LazyColumnExample() {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(16.dp)
) {
items(100) { index ->
Text("Item $index")
}
}
}
// LazyRow - Horizontal scrolling
@Composable
fun LazyRowExample() {
LazyRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(16.dp)
) {
items(20) { index ->
Box(
modifier = Modifier
.size(100.dp)
.background(Color.Blue)
) {
Text("$index", color = Color.White)
}
}
}
}
// LazyVerticalGrid
@Composable
fun GridExample() {
LazyVerticalGrid(
columns = GridCells.Fixed(3),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
contentPadding = PaddingValues(16.dp)
) {
items(100) { index ->
Box(
modifier = Modifier
.aspectRatio(1f)
.background(Color.Blue),
contentAlignment = Alignment.Center
) {
Text("$index", color = Color.White)
}
}
}
}
// Adaptive grid
@Composable
fun AdaptiveGridExample() {
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = 100.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
items(100) { index ->
Box(
modifier = Modifier
.aspectRatio(1f)
.background(Color.Green)
)
}
}
}
State Management
import androidx.compose.runtime.*
import androidx.compose.material3.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewmodel.compose.viewModel
// remember - Simple local state
@Composable
fun CounterScreen() {
var count by remember { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
// rememberSaveable - Survives configuration changes
@Composable
fun PersistentCounter() {
var count by rememberSaveable { mutableStateOf(0) }
Column {
Text("Count: $count")
Button(onClick = { count++ }) {
Text("Increment")
}
}
}
// ViewModel
class CounterViewModel : ViewModel() {
private val _count = mutableStateOf(0)
val count: State = _count
fun increment() {
_count.value++
}
}
@Composable
fun CounterWithViewModel(
viewModel: CounterViewModel = viewModel()
) {
Column {
Text("Count: ${viewModel.count.value}")
Button(onClick = { viewModel.increment() }) {
Text("Increment")
}
}
}
// StateFlow + collectAsState
class PostsViewModel : ViewModel() {
private val _posts = MutableStateFlow>(emptyList())
val posts: StateFlow> = _posts.asStateFlow()
fun loadPosts() {
viewModelScope.launch {
// Fetch posts
_posts.value = fetchPosts()
}
}
}
@Composable
fun PostsScreen(
viewModel: PostsViewModel = viewModel()
) {
val posts by viewModel.posts.collectAsState()
LazyColumn {
items(posts) { post ->
Text(post.title)
}
}
LaunchedEffect(Unit) {
viewModel.loadPosts()
}
}
Material Design 3
Theming
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
// Color scheme
private val LightColors = lightColorScheme(
primary = Color(0xFF6200EE),
secondary = Color(0xFF03DAC6),
background = Color(0xFFFFFFFF),
surface = Color(0xFFFFFFFF),
error = Color(0xFFB00020),
onPrimary = Color.White,
onSecondary = Color.Black,
onBackground = Color.Black,
onSurface = Color.Black,
onError = Color.White
)
private val DarkColors = darkColorScheme(
primary = Color(0xFFBB86FC),
secondary = Color(0xFF03DAC6),
background = Color(0xFF121212),
surface = Color(0xFF121212),
error = Color(0xFFCF6679),
onPrimary = Color.Black,
onSecondary = Color.Black,
onBackground = Color.White,
onSurface = Color.White,
onError = Color.Black
)
@Composable
fun MyAppTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
content: @Composable () -> Unit
) {
val colors = if (darkTheme) DarkColors else LightColors
MaterialTheme(
colorScheme = colors,
typography = Typography,
content = content
)
}
// Typography
val Typography = Typography(
displayLarge = TextStyle(
fontSize = 57.sp,
lineHeight = 64.sp,
fontWeight = FontWeight.Bold
),
headlineLarge = TextStyle(
fontSize = 32.sp,
lineHeight = 40.sp,
fontWeight = FontWeight.Bold
),
bodyLarge = TextStyle(
fontSize = 16.sp,
lineHeight = 24.sp,
fontWeight = FontWeight.Normal
)
)
Material Components
import androidx.compose.material3.*
import androidx.compose.runtime.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MaterialComponentsDemo() {
var showDialog by remember { mutableStateOf(false) }
var showBottomSheet by remember { mutableStateOf(false) }
Scaffold(
topBar = {
TopAppBar(
title = { Text("App Title") },
actions = {
IconButton(onClick = { }) {
Icon(Icons.Default.Search, "Search")
}
}
)
},
floatingActionButton = {
FloatingActionButton(onClick = { }) {
Icon(Icons.Default.Add, "Add")
}
},
bottomBar = {
NavigationBar {
NavigationBarItem(
selected = true,
onClick = { },
icon = { Icon(Icons.Default.Home, null) },
label = { Text("Home") }
)
NavigationBarItem(
selected = false,
onClick = { },
icon = { Icon(Icons.Default.Search, null) },
label = { Text("Search") }
)
}
}
) { padding ->
Column(
modifier = Modifier
.padding(padding)
.padding(16.dp)
) {
// Card
Card(
modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(4.dp)
) {
Text(
"Card Content",
modifier = Modifier.padding(16.dp)
)
}
Spacer(Modifier.height(16.dp))
// Buttons
Button(onClick = { showDialog = true }) {
Text("Show Dialog")
}
OutlinedButton(onClick = { showBottomSheet = true }) {
Text("Show Bottom Sheet")
}
}
}
// Dialog
if (showDialog) {
AlertDialog(
onDismissRequest = { showDialog = false },
title = { Text("Dialog Title") },
text = { Text("Dialog content goes here") },
confirmButton = {
TextButton(onClick = { showDialog = false }) {
Text("OK")
}
},
dismissButton = {
TextButton(onClick = { showDialog = false }) {
Text("Cancel")
}
}
)
}
// Modal Bottom Sheet
if (showBottomSheet) {
ModalBottomSheet(
onDismissRequest = { showBottomSheet = false }
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text("Bottom Sheet Content")
Button(
onClick = { showBottomSheet = false },
modifier = Modifier.fillMaxWidth()
) {
Text("Close")
}
}
}
}
}
Navigation
Navigation Compose
import androidx.navigation.compose.*
// Define screens
sealed class Screen(val route: String) {
object Home : Screen("home")
object Profile : Screen("profile/{userId}") {
fun createRoute(userId: String) = "profile/$userId"
}
object Settings : Screen("settings")
}
// Setup navigation
@Composable
fun AppNavigation() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Home.route
) {
composable(Screen.Home.route) {
HomeScreen(
onNavigateToProfile = { userId ->
navController.navigate(Screen.Profile.createRoute(userId))
}
)
}
composable(
route = Screen.Profile.route,
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) { backStackEntry ->
val userId = backStackEntry.arguments?.getString("userId")
ProfileScreen(
userId = userId,
onNavigateBack = { navController.popBackStack() }
)
}
composable(Screen.Settings.route) {
SettingsScreen()
}
}
}
@Composable
fun HomeScreen(onNavigateToProfile: (String) -> Unit) {
Column {
Text("Home Screen")
Button(onClick = { onNavigateToProfile("user123") }) {
Text("Go to Profile")
}
}
}
@Composable
fun ProfileScreen(
userId: String?,
onNavigateBack: () -> Unit
) {
Column {
Text("Profile: $userId")
Button(onClick = onNavigateBack) {
Text("Back")
}
}
}
Best Practices
Performance Tips
1. Use LazyColumn/LazyRow:
✓ For scrollable lists
✓ Only composes visible items
✗ Don't use Column in ScrollView
2. Avoid recomposition:
✓ Use remember for expensive calculations
✓ Use derivedStateOf for derived values
✓ Pass stable parameters
✗ Don't create new lambdas unnecessarily
3. Use keys in lists:
LazyColumn {
items(
items = users,
key = { user -> user.id } // Stable key
) { user ->
UserItem(user)
}
}
4. Optimize images:
✓ Use Coil or Glide for async loading
✓ Set proper contentScale
✓ Cache images
5. Profile your app:
✓ Use Layout Inspector
✓ Check recomposition counts
✓ Measure with Macrobenchmark
Conclusion
Jetpack Compose represents the future of Android UI development with its declarative approach, Kotlin-first design, and excellent Material Design 3 support. The framework is mature, performant, and backed by Google's full commitment. Whether starting a new project or migrating an existing app, Compose significantly improves productivity while reducing code complexity. The learning curve is worth it—start building with Compose today.
Modern Compose apps need modern support infrastructure. Our support URL generator creates professional support pages that meet Google Play requirements, ensuring your cutting-edge Compose app has the support foundation users expect.