Back to all articles

Jetpack Compose: Modern Android UI Development Guide 2025

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.

Need a Support URL for Your App?

Generate a compliant, professional support page in under a minute. Our easy-to-use generator creates everything you need for App Store and Google Play submissions.