Skip to content

Jetpack Compose Basics — Kotlin Declarative UI Guide

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Jetpack Compose Basics. We cover key concepts, practical examples, and best practices to help you master this topic.

Jetpack Compose is Android's modern declarative UI toolkit written in Kotlin that builds interfaces with composable functions, automatic state-driven recomposition, and a modifier chain for styling and layout.

What You'll Learn

  • Write composable functions with @Composable
  • Manage state with remember and mutableStateOf
  • Use Modifier chaining for styling and layout
  • Build layouts with Column, Row, and Box
  • Handle user input with TextField and Button
  • Understand recomposition and state hoisting
  • Apply Material Design 3 theming

Why It Matters

Compose replaces the legacy XML-based View system with Kotlin code. It reduces boilerplate, eliminates findViewById, and makes UI code more readable and maintainable. State-driven updates mean you describe what the UI should look like for each state, and Compose handles the rest. This paradigm shift is fundamental for modern Android development.

Real-World Use

DodaTech's Android configuration utility was rewritten from XML layouts to Compose. The result is 60% less code, simpler state management, and faster development cycles for new features. All new DodaTech Android projects use Compose exclusively.

Learning Path

flowchart LR
  A[Activities & Fragments] --> B[Compose Basics\nYou are here]
  B --> C[Compose Layout]
  style B fill:#f90,color:#fff

Your First Composable

A composable function is annotated with @Composable and describes a part of the UI.

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.sp

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Greeting("Kotlin Compose")
        }
    }
}

@Composable
fun Greeting(name: String) {
    Text(
        text = "Hello, $name!",
        fontSize = 24.sp,
        modifier = Modifier
            .padding(16.dp)
    )
}

Output: The screen displays "Hello, Kotlin Compose!" with 24sp font size and 16dp padding.

State and Recomposition

State drives UI updates. When state changes, Compose automatically recomposes the affected composables.

import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.*

@Composable
fun Counter() {
    var count by remember { mutableIntStateOf(0) }

    Column(
        modifier = Modifier.padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Count: $count",
            fontSize = 32.sp
        )
        
        Spacer(modifier = Modifier.height(16.dp))
        
        Button(onClick = { count++ }) {
            Text("Increment")
        }
        
        Spacer(modifier = Modifier.height(8.dp))
        
        Button(onClick = { count = 0 }) {
            Text("Reset")
        }
    }
}

Output: The count starts at 0. Each button click increments or resets the count. The Text updates automatically because Compose observes the mutable state.

Modifiers

Modifiers style and configure composables through a chain of calls.

@Composable
fun StyledCard() {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
            .clickable { /* handle click */ },
        elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
    ) {
        Column(
            modifier = Modifier.padding(16.dp)
        ) {
            Text(
                text = "Jetpack Compose",
                fontSize = 20.sp,
                fontWeight = FontWeight.Bold
            )
            
            Spacer(modifier = Modifier.height(8.dp))
            
            Text(
                text = "A modern toolkit for building native Android UI. " +
                        "Compose simplifies and accelerates UI development.",
                fontSize = 16.sp,
                color = Color.Gray
            )
            
            Spacer(modifier = Modifier.height(12.dp))
            
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.End
            ) {
                Text(
                    text = "Learn more →",
                    color = Color.Blue,
                    fontWeight = FontWeight.Medium
                )
            }
        }
    }
}

Output: A Material Card with title, description, and a "Learn more" link. Modifiers control size, padding, click handling, and arrangement.

Layouts: Column, Row, and Box

The three fundamental layout composables arrange children vertically, horizontally, or stacked.

@Composable
fun LayoutExamples() {
    // Vertical layout
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp),
        horizontalAlignment = Alignment.CenterHorizontally
    ) {
        Text("Column Layout")
        
        // Horizontal layout
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {
            Box(
                modifier = Modifier
                    .size(80.dp)
                    .background(Color.Red),
                contentAlignment = Alignment.Center
            ) {
                Text("A", color = Color.White)
            }
            
            Box(
                modifier = Modifier
                    .size(80.dp)
                    .background(Color.Green),
                contentAlignment = Alignment.Center
            ) {
                Text("B", color = Color.White)
            }
            
            Box(
                modifier = Modifier
                    .size(80.dp)
                    .background(Color.Blue),
                contentAlignment = Alignment.Center
            ) {
                Text("C", color = Color.White)
            }
        }
        
        // Box with layered content
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(150.dp)
                .background(Color.LightGray)
        ) {
            Text(
                "Background text",
                modifier = Modifier.align(Alignment.Center)
            )
            Box(
                modifier = Modifier
                    .size(40.dp)
                    .background(Color.Yellow)
                    .align(Alignment.TopEnd)
            )
        }
    }
}

Output: Column stacks children vertically. Row arranges them horizontally. Box layers children on top of each other with alignment control.

TextField and User Input

TextField collects user input with state management.

@Composable
fun LoginForm() {
    var username by remember { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var isPasswordVisible by remember { mutableStateOf(false) }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp),
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "Welcome",
            fontSize = 28.sp,
            fontWeight = FontWeight.Bold
        )
        
        Spacer(modifier = Modifier.height(24.dp))
        
        OutlinedTextField(
            value = username,
            onValueChange = { username = it },
            label = { Text("Username") },
            modifier = Modifier.fillMaxWidth(),
            singleLine = true
        )
        
        Spacer(modifier = Modifier.height(12.dp))
        
        OutlinedTextField(
            value = password,
            onValueChange = { password = it },
            label = { Text("Password") },
            modifier = Modifier.fillMaxWidth(),
            singleLine = true,
            visualTransformation = if (isPasswordVisible) VisualTransformation.None
                else PasswordVisualTransformation(),
            trailingIcon = {
                IconButton(onClick = { isPasswordVisible = !isPasswordVisible }) {
                    Icon(
                        imageVector = if (isPasswordVisible)
                            Icons.Default.VisibilityOff else Icons.Default.Visibility,
                        contentDescription = "Toggle password visibility"
                    )
                }
            }
        )
        
        Spacer(modifier = Modifier.height(24.dp))
        
        Button(
            onClick = { /* handle login */ },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text("Log In")
        }
    }
}

Output: A login form with username and password fields, password visibility toggle, and a login button. State updates re-render the UI automatically.

State Hoisting

State hoisting lifts state to a higher composable for sharing and testability.

// Lower-level composable: no state, just display and callbacks
@Composable
fun TemperatureDisplay(
    celsius: Double,
    onCelsiusChange: (Double) -> Unit,
    modifier: Modifier = Modifier
) {
    Column(modifier = modifier.padding(16.dp)) {
        Text(
            text = "Temperature: ${String.format("%.1f", celsius)} C",
            fontSize = 24.sp
        )
        
        Slider(
            value = celsius.toFloat(),
            onValueChange = { onCelsiusChange(it.toDouble()) },
            valueRange = -10f..40f,
            modifier = Modifier.fillMaxWidth()
        )
    }
}

// Higher-level composable: owns the state
@Composable
fun TemperatureScreen() {
    var temp by remember { mutableStateOf(22.0) }
    
    Column(
        modifier = Modifier.fillMaxSize().padding(16.dp)
    ) {
        TemperatureDisplay(
            celsius = temp,
            onCelsiusChange = { temp = it }
        )
        
        Text(
            text = "Fahrenheit: ${String.format("%.1f", temp * 9 / 5 + 32)} F",
            fontSize = 18.sp,
            color = Color.Gray
        )
    }
}

Output: The slider changes the temperature. The Fahrenheit conversion is computed from the hoisted state.

Theming with Material 3

Material Design 3 theming provides consistent colors, typography, and shapes.

@Composable
fun AppTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colorScheme = if (darkTheme) darkColorScheme() else lightColorScheme()

    MaterialTheme(
        colorScheme = colorScheme,
        typography = Typography(),
        content = content
    )
}

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            AppTheme {
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Greeting("Compose Theme")
                }
            }
        }
    }
}

Output: The app uses Material 3 theming with automatic dark/light mode support.

Common Mistakes

  1. Creating state without remember: mutableStateOf without remember creates a new state object on every recomposition, losing the value. Always wrap with remember.

  2. Long lambda callbacks without memoization: If a lambda is passed to a composable that skips recomposition, wrap it with remember to avoid unnecessary recompositions.

  3. Modifier order matters: The order of modifier calls affects behavior. Clickable after padding gives the full padded area as clickable. Clickable before padding gives only the unpadded area.

  4. Using Box instead of Column/Row for simple layouts: Box is for overlapping content. Use Column for vertical stacks and Row for horizontal arrangements.

  5. Not using mutableStateOf for state: Regular variables in composables do not trigger recomposition. State must be wrapped in mutableStateOf, StateFlow, or LiveData.

  6. Ignoring previews: @Preview annotations let you see composables in Android Studio's design view. Add previews for all major composables.

Practice Questions

  1. What triggers recomposition in a composable?

Answer: A state object (mutableStateOf, StateFlow, LiveData) changes value and the composable reads that state. Compose automatically recomposes the affected composables.

  1. What is the purpose of the remember function?

Answer: remember preserves a value across recompositions. Without it, variables are reinitialized on every recomposition.

  1. What is state hoisting?

Answer: State hoisting moves state to the caller of a composable, making the composable stateless and reusable. The composable receives state and callbacks as parameters.

  1. How do modifiers work in Compose?

Answer: Modifiers use a chain pattern. Each modifier wraps the previous one, adding behavior or styling. The order of modifiers affects the final result.

  1. Challenge: Create a color picker with three sliders (Red, Green, Blue) that displays the resulting color in a preview box. Hoist the RGB state to the parent composable.

Answer:

@Composable
fun ColorSlider(
    label: String,
    value: Int,
    onValueChange: (Int) -> Unit,
    color: Color
) {
    Row(
        verticalAlignment = Alignment.CenterVertically,
        modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp)
    ) {
        Text("$label: $value", width = 80.dp)
        Slider(
            value = value.toFloat(),
            onValueChange = { onValueChange(it.toInt()) },
            valueRange = 0f..255f,
            colors = SliderDefaults.colors(
                thumbColor = color,
                activeTrackColor = color
            ),
            modifier = Modifier.weight(1f)
        )
    }
}

@Composable
fun ColorPicker() {
    var red by remember { mutableIntStateOf(128) }
    var green by remember { mutableIntStateOf(128) }
    var blue by remember { mutableIntStateOf(128) }
    
    Column {
        ColorSlider("Red", red, { red = it }, Color.Red)
        ColorSlider("Green", green, { green = it }, Color.Green)
        ColorSlider("Blue", blue, { blue = it }, Color.Blue)
        
        Box(
            modifier = Modifier
                .fillMaxWidth()
                .height(100.dp)
                .padding(16.dp)
                .background(Color(red, green, blue))
        )
        
        Text(
            "RGB($red, $green, $blue)",
            modifier = Modifier.align(Alignment.CenterHorizontally)
        )
    }
}

Mini Project

Build a simple calculator app in Compose. Requirements:

  • Number buttons 0-9 arranged in a grid
  • Operation buttons: +, -, *, /
  • Clear and equals buttons
  • Display area showing input and result
  • State management for current input, previous value, and selected operation
  • Handle division by zero gracefully
  • Material 3 theming with dark mode support

This project applies all Compose basics: state, layout, modifiers, event handling, and theming.

FAQ

What is the difference between mutableStateOf and mutableIntStateOf?

mutableIntStateOf is an optimized variant for Int values. Use mutableStateOf for other types. Both trigger recomposition when their value changes.

Can I use Compose with XML layouts?

Yes. You can embed Compose in existing XML layouts with ComposeView, or embed Android Views in Compose with AndroidView.

How do I handle user input in Compose?

Use TextField, OutlinedTextField, or BasicTextField. Bind them to a state variable with the onValueChange callback.

What is the minimum Android version for Compose?

Compose requires API 21 (Android 5.0) minimum. Jetpack Compose 1.6+ supports back to API 21.

How do I create custom layouts in Compose?

Use Layout composable with a measure and place lambda, or use the Modifier.layout() extension for simple custom layouts.

What's Next

After mastering Compose basics, learn Compose layout for complex arrangements. You can also explore ViewModel for production state management.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro