Jetpack Compose: Modern Android UI Toolkit
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
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))
}
}
}| Composable | Axis | Analogy in SwiftUI |
|---|---|---|
Column | Vertical | VStack |
Row | Horizontal | HStack |
Box | Z-order | ZStack |
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
Try It Yourself
Build a Compose app from scratch:
- Create a new Android project with “Empty Compose Activity” template
- Replace the default
Greetingwith your own composable - Add a
LazyColumnshowing a list of items - Implement search with
rememberandderivedStateOf - 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