Jetpack Compose Basics — Kotlin Declarative UI Guide
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
Creating state without remember: mutableStateOf without remember creates a new state object on every recomposition, losing the value. Always wrap with remember.
Long lambda callbacks without memoization: If a lambda is passed to a composable that skips recomposition, wrap it with remember to avoid unnecessary recompositions.
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.
Using Box instead of Column/Row for simple layouts: Box is for overlapping content. Use Column for vertical stacks and Row for horizontal arrangements.
Not using mutableStateOf for state: Regular variables in composables do not trigger recomposition. State must be wrapped in mutableStateOf, StateFlow, or LiveData.
Ignoring previews: @Preview annotations let you see composables in Android Studio's design view. Add previews for all major composables.
Practice Questions
- 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.
- What is the purpose of the remember function?
Answer: remember preserves a value across recompositions. Without it, variables are reinitialized on every recomposition.
- 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.
- 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.
- 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'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