Skip to content
Jetpack Compose: Modern Android UI Toolkit

Jetpack Compose: Modern Android UI Toolkit

DodaTech Updated Jun 20, 2026 8 min read

Jetpack Compose is Android’s modern declarative UI toolkit that replaces XML layouts with Kotlin code — using composable functions, reactive state management, and Material Design 3 to build native UIs with less code and fewer bugs.

What You’ll Learn

You’ll master Column/Row/Box layouts, @Composable functions, state with remember and mutableStateOf, Material Design 3 components, Navigation Compose, theming, previews, and interoperability with traditional XML layouts.

Why Jetpack Compose Matters

Google has declared Compose the future of Android UI development. Doda Browser and Durga Antivirus Pro for Android could be built with significantly less code using Compose. Since 2022, new Android projects default to Compose, and Google’s new UI APIs are Compose-first. Learning Compose today ensures your skills stay relevant.

Jetpack Compose Learning Path

    flowchart LR
  A[Mobile Development Overview] --> B[Android Development]
  B --> C[Jetpack Compose]
  C --> D[State & Data Flow]
  D --> E[Navigation & Lists]
  E --> F[Production Apps]
  C:::current
  classDef current fill:#f90,color:#fff,stroke:#333,stroke-width:2px
  
Prerequisites: Basic Kotlin knowledge — variables, functions, lambdas. Android Studio Hedgehog (2023.3.1) or newer. Understanding of Android development fundamentals is helpful.

Declarative vs Imperative UI

In the traditional XML + View system, you imperatively manipulate views:

// Old Android: Find view, set text, set click listener
val textView = findViewById<TextView>(R.id.greeting)
textView.text = "Hello"
button.setOnClickListener { textView.text = "Clicked!" }

With Compose, you declare the UI and the framework handles updates:

// Compose: Declare what the UI should look like
@Composable
fun Greeting() {
    var text by remember { mutableStateOf("Hello") }

    Column {
        Text(text)
        Button(onClick = { text = "Clicked!" }) {
            Text("Tap Me")
        }
    }
}

When text changes, Compose automatically re-renders only the affected parts — no findViewById, no manual updates.

Your First Composable

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            DodaTechTheme {
                GreetingScreen()
            }
        }
    }
}

@Composable
fun GreetingScreen() {
    var name by remember { mutableStateOf("") }

    Column(
        modifier = Modifier.padding(24.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Text(
            text = "Welcome to DodaTech",
            fontSize = 28.sp,
            color = MaterialTheme.colorScheme.primary
        )

        OutlinedTextField(
            value = name,
            onValueChange = { name = it },
            label = { Text("Enter your name") },
            modifier = Modifier.fillMaxWidth()
        )

        if (name.isNotBlank()) {
            Text(
                text = "Hello, $name!",
                fontSize = 20.sp,
                color = MaterialTheme.colorScheme.secondary
            )
        }

        Button(
            onClick = { name = "" },
            modifier = Modifier.align(Alignment.End)
        ) {
            Text("Clear")
        }
    }
}

Expected output: A screen with a title, text input, dynamic greeting that appears as you type, and a clear button.

Layout Composables: Column, Row, Box

@Composable
fun LayoutExamples() {
    Column(modifier = Modifier.padding(16.dp)) {
        Text("Column: Vertical arrangement", style = MaterialTheme.typography.titleMedium)

        Row(
            horizontalArrangement = Arrangement.spacedBy(12.dp),
            modifier = Modifier.fillMaxWidth()
        ) {
            Box(
                modifier = Modifier.size(60.dp).background(Color.Blue, shape = CircleShape)
            )
            Box(
                modifier = Modifier.size(60.dp).background(Color.Green, shape = CircleShape)
            )
            Box(
                modifier = Modifier.size(60.dp).background(Color.Orange, shape = CircleShape)
            )
        }

        Spacer(modifier = Modifier.height(16.dp))

        Box(modifier = Modifier.size(120.dp)) {
            // Z-order: later children appear on top
            Box(modifier = Modifier.matchParentSize().background(Color.Yellow))
            Text("Overlay", modifier = Modifier.align(Alignment.Center))
        }
    }
}
ComposableAxisAnalogy in SwiftUI
ColumnVerticalVStack
RowHorizontalHStack
BoxZ-orderZStack

State Management

Compose provides several ways to manage state:

// 1. remember + mutableStateOf — local state
var count by remember { mutableStateOf(0) }

// 2. remember + derivedStateOf — computed state
val isEven by remember { derivedStateOf { count % 2 == 0 } }

// 3. ViewModel + collectAsState — screen-level state
class CounterViewModel : ViewModel() {
    private val _count = MutableStateFlow(0)
    val count: StateFlow<Int> = _count.asStateFlow()
}

@Composable
fun CounterScreen(viewModel: CounterViewModel = viewModel()) {
    val count by viewModel.count.collectAsState()
    Text("Count: $count")
}
    flowchart TD
    UI[Composable UI] -->|reads| State["State<br/>(mutableStateOf / StateFlow)"]
    Event[User Event] -->|onClick / onValueChange| State
    State -->|recomposition| UI
    ViewModel[ViewModel] -->|StateFlow| State
  

Key principle: State is hoisted up — the composable that needs it owns it. Child composables receive values through parameters.

Material Design 3 Components

Compose includes Material Design 3 out of the box:

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModernUI() {
    var selected by remember { mutableStateOf(false) }

    Scaffold(
        topBar = {
            TopAppBar(
                title = { Text("DodaTech App") },
                colors = TopAppBarDefaults.topAppBarColors(
                    containerColor = MaterialTheme.colorScheme.primaryContainer
                )
            )
        },
        floatingActionButton = {
            FloatingActionButton(onClick = { /* action */ }) {
                Icon(Icons.Default.Add, contentDescription = "Add")
            }
        }
    ) { padding ->
        Column(modifier = Modifier.padding(padding)) {
            Card(modifier = Modifier.padding(16.dp).fillMaxWidth()) {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text("Settings", style = MaterialTheme.typography.titleMedium)
                    Switch(
                        checked = selected,
                        onCheckedChange = { selected = it },
                        modifier = Modifier.fillMaxWidth()
                    )
                    Slider(value = 0.5f, onValueChange = {})
                }
            }
        }
    }
}

Navigation Compose

// build.gradle.kts
// implementation("androidx.navigation:navigation-compose:2.7.7")

@Composable
fun AppNavigation() {
    val navController = rememberNavController()

    NavHost(navController = navController, startDestination = "home") {
        composable("home") {
            HomeScreen(
                onProductClick = { productId ->
                    navController.navigate("product/$productId")
                }
            )
        }
        composable(
            route = "product/{productId}",
            arguments = listOf(navArgument("productId") { type = NavType.IntType })
        ) { backStackEntry ->
            val productId = backStackEntry.arguments?.getInt("productId") ?: 0
            ProductScreen(productId = productId, onBack = { navController.popBackStack() })
        }
    }
}

Theming

@Composable
fun DodaTechTheme(content: @Composable () -> Unit) {
    val colorScheme = lightColorScheme(
        primary = Color(0xFF1565C0),
        secondary = Color(0xFF43A047),
        background = Color(0xFFF5F5F5),
        surface = Color.White,
        onPrimary = Color.White,
        onSecondary = Color.White,
    )

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography(
            headlineLarge = TextStyle(fontSize = 32.sp, fontWeight = FontWeight.Bold),
            titleMedium = TextStyle(fontSize = 18.sp, fontWeight = FontWeight.SemiBold),
        ),
        content = content
    )
}

Previews

@Preview(name = "Light Mode", showBackground = true)
@Preview(name = "Dark Mode", showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
fun GreetingScreenPreview() {
    DodaTechTheme {
        GreetingScreen()
    }
}

@Preview(device = "spec:width=411dp,height=891dp", showSystemUi = true)
@Composable
fun FullScreenPreview() {
    DodaTechTheme {
        GreetingScreen()
    }
}

Interoperability with XML Layouts

You can embed Compose in existing XML layouts and vice versa.

Compose in XML

<!-- layout/activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">

    <TextView android:text="Header" ... />

    <androidx.compose.ui.platform.ComposeView
        android:id="@+id/compose_view"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>
// In Activity
findViewById<ComposeView>(R.id.compose_view).setContent {
    DodaTechTheme {
        MyComposableContent()
    }
}

XML in Compose

@Composable
fun AndroidViewExample() {
    AndroidView(
        factory = { context ->
            LayoutInflater.from(context)
                .inflate(R.layout.legacy_view, null) as LinearLayout
        },
        modifier = Modifier.fillMaxWidth()
    )
}

Common Jetpack Compose Errors

1. Forgetting remember Causes State Reset on Recomposition

Without remember, mutableStateOf creates a new state on every recomposition. Always wrap with remember or use rememberSaveable for configuration changes.

2. Modifier Order Confusion

// Wrong: padding applied after background
Box(modifier = Modifier.background(Color.Red).padding(16.dp))

// Correct: background covers the padded area
Box(modifier = Modifier.padding(16.dp).background(Color.Red))

Modifiers are applied left-to-right — each wraps the previous.

3. LaunchedEffect Without a Key

// Wrong: runs on every recomposition
LaunchedEffect(Unit) { /* side effect */ }

// Correct: runs only once (Unit never changes)
LaunchedEffect(Unit) { /* side effect */ }

// Correct: runs when userId changes
LaunchedEffect(userId) { /* fetch data */ }

4. State Hoisting Violations

Don’t pass mutableStateOf down; pass the value and a callback. This makes composables reusable and testable.

5. Long Operations in @Composable Functions

Composable functions can be called on any thread and at any time. Never put blocking I/O or heavy computation directly in a @Composable function — use LaunchedEffect, ViewModel, or coroutines.

6. Missing Modifier Parameter

Every reusable composable should accept a Modifier parameter so callers can customize padding, size, clicks, etc.

7. Ignoring Baseline Profiles

Compose apps benefit significantly from baseline profiles that pre-compile critical paths. Add baselineProfileConfig to your Gradle build.

Practice Questions

1. What’s the difference between remember and rememberSaveable?

remember retains state during recomposition but loses it on configuration changes (screen rotation). rememberSaveable survives configuration changes by saving to a Bundle.

2. What is state hoisting in Compose?

The pattern of moving state to a composable’s caller so the composable itself is stateless and reusable. The parent owns the state; the child receives the value and an onChange callback.

3. How do you navigate between screens in Compose?

Use Navigation Compose: define a NavHost with composable() routes and navigate with navController.navigate(). Pass arguments via route patterns.

4. What’s the purpose of the Modifier parameter?

Modifiers configure how a composable behaves and appears — padding, size, click handling, background, etc. Accepting a Modifier parameter makes composables reusable and composable.

5. Challenge: Build a product listing app.

Create a Compose app with: LazyColumn showing products from a list, navigation to a detail screen with NavHost, state management via ViewModel, Material Design 3 theming, and a search bar that filters the list in real time.

FAQ

Can I use Jetpack Compose with Java?
No, Compose requires Kotlin. Compose relies on Kotlin-specific features (coroutines, lambdas, trailing closures, extension functions) that have no Java equivalent.
Is Jetpack Compose ready for production?
Yes, since Compose 1.0 (July 2021) it’s production-ready. Major apps like Twitter, Reddit, and Dropbox use Compose. Google recommends it for all new Android apps.
How do I handle ViewModels in Compose?
Use the viewModel() composable function from androidx.lifecycle.viewmodel-compose. It returns the ViewModel scoped to the nearest ViewModelStoreOwner (usually the Activity or NavBackStackEntry).
What’s the difference between LaunchedEffect and SideEffect?
LaunchedEffect launches a coroutine in the composition scope (for async work like API calls). SideEffect runs synchronously on every recomposition (for things like updating callbacks).

Try It Yourself

Build a Compose app from scratch:

  1. Create a new Android project with “Empty Compose Activity” template
  2. Replace the default Greeting with your own composable
  3. Add a LazyColumn showing a list of items
  4. Implement search with remember and derivedStateOf
  5. Add navigation to a detail screen

What’s Next

Jetpack Compose is the standard for modern Android development. Start by converting one XML screen in your existing app to Compose — you’ll immediately see the productivity benefits of the declarative approach.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro