Skip to content
ExponaBLOG
MODERN ANDROID & KOTLIN · DAY 016 / 100

Jetpack Compose basics: a practical guide for developers

Declarative UI for Android.

Expona Admin8 min read

Why Jetpack Compose matters now

If you're building Android apps, you've likely felt the friction of managing XML layouts, findViewById calls, and state synchronization across views. Jetpack Compose basics: a practical guide for developers introduces a declarative paradigm that shifts how we think about building UI. Instead of imperative view manipulation, we describe what the UI should look like at any given state—and Compose handles the rendering.

This shift mirrors what React did for web development and Flutter for cross-platform mobile. After years of working with traditional Android views in production e-commerce and ride-hailing apps, we've found Compose dramatically reduces bugs around UI state consistency and accelerates feature delivery. By day 16 of your Modern Android journey, understanding these fundamentals gives you the foundation to build scalable, maintainable interfaces.

Composable functions—UI is a function of state

The core concept of Jetpack Compose is deceptively simple: a composable function is a Kotlin function annotated with @Composable that describes a piece of UI. Unlike traditional Android views, composables are pure functions (or as close as we can get in practice). Given the same inputs, they produce the same output.

Let's start with the simplest example:

kotlin
@Composable
fun Greeting(name: String) {
    Text(text = "Hello, $name")
}

This function takes a string parameter and returns a Text composable. Call it from another composable and you get UI. The key insight is that the function is not imperative—we're not saying "create a TextView and set its text." We're declaring what should be displayed.

In a real app, you'll compose these together. Let's build a user profile card:

kotlin
@Composable
fun UserProfileCard(userName: String, userBio: String, followerCount: Int) {
    Column(
        modifier = Modifier
            .padding(16.dp)
            .fillMaxWidth()
    ) {
        Text(
            text = userName,
            style = MaterialTheme.typography.headlineSmall
        )
        Text(
            text = userBio,
            style = MaterialTheme.typography.bodyMedium,
            color = Color.Gray
        )
        Text(
            text = "Followers: $followerCount",
            style = MaterialTheme.typography.labelSmall
        )
    }
}

This composable receives data and describes the layout. It doesn't manage views or listeners directly. The composition tree—Column, Text elements—is built and laid out by the Compose runtime.

In production systems handling millions of transactions per second, this declarative model shines because state flows in one direction: data → UI. No more fragmented state spread across multiple view objects. We've seen this reduce subtle bugs in cart updates and payment flows where UI and data got out of sync.

Recomposition—Compose redraws when state changes

Recomposition is how Compose keeps the UI in sync with state. When a state variable changes, Compose automatically reruns the composable function and updates the display. This is efficient because Compose only recomposes what changed—a process called smart recomposition.

To manage state, use the remember and mutableStateOf APIs:

kotlin
@Composable
fun ClickCounter() {
    var count by remember { mutableStateOf(0) }

    Column(
        modifier = Modifier.padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Text(text = "Count: $count")
        Button(onClick = { count += 1 }) {
            Text("Increment")
        }
    }
}

Here, remember { mutableStateOf(0) } creates a state holder that survives recomposition. When the button is clicked, count changes, triggering a recomposition. Compose reruns ClickCounter, reads the new count value, and updates only the Text displaying it.

In a more complex scenario—say, a product list in an e-commerce app—recomposition becomes crucial:

kotlin
@Composable
fun ProductList(productIds: List) {
    var selectedProduct by remember { mutableStateOf(null) }

    Column {
        LazyColumn {
            items(productIds) { productId ->
                ProductRow(
                    productId = productId,
                    isSelected = selectedProduct == productId,
                    onSelect = { selectedProduct = productId }
                )
            }
        }
        if (selectedProduct != null) {
            ProductDetail(productId = selectedProduct!!)
        }
    }
}

When the user taps a product, selectedProduct updates. Compose recomposes only the affected branches: the ProductRow that was selected, the one that was deselected, and the ProductDetail section. The rest of the list stays untouched. This efficiency is essential when handling large data sets or frequent updates.

A critical gotcha: don't call remember inside conditions or loops outside of Compose's predictable execution model. The order and number of state holders must remain consistent across recompositions.

Previews—@Preview shows UI in Android Studio

The @Preview annotation is a game-changer for development velocity. Annotate any composable with @Preview and Android Studio renders it instantly in the design pane, without running an emulator or device. You iterate on UI in milliseconds.

kotlin
@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    Greeting(name = "Alice")
}

Click the refresh icon in the preview pane and you see changes immediately. For complex screens, create multiple previews with different states:

kotlin
@Preview(showBackground = true, widthDp = 320, heightDp = 640)
@Composable
fun UserProfileCardPreview_Light() {
    UserProfileCard(
        userName = "John Doe",
        userBio = "Mobile engineer",
        followerCount = 1250
    )
}

@Preview(showBackground = true, widthDp = 320, heightDp = 640, uiMode = UI_MODE_NIGHT_YES)
@Composable
fun UserProfileCardPreview_Dark() {
    UserProfileCard(
        userName = "Jane Smith",
        userBio = "Product designer",
        followerCount = 3400
    )
}

We specify device dimensions and even dark mode via the uiMode parameter. In our projects, previews have cut design review cycles by 50% because designers and developers can validate UI changes before merging code. This feedback loop is invaluable.

Previews don't run your app logic—they're static snapshots. For interactive previews, you'll still need to run on a device or emulator, but for layout and styling validation, they're unbeatable.

Modifiers—Size, padding, click handling and more

Modifiers are how you customize composable appearance and behavior. They chain together to build complex layouts and interactions. Think of them like the CSS box model plus event handlers, all in Kotlin.

Basic sizing and spacing:

kotlin
@Composable
fun PaddedButton() {
    Button(
        modifier = Modifier
            .padding(horizontal = 16.dp, vertical = 8.dp)
            .size(width = 200.dp, height = 48.dp),
        onClick = { /* handle click */ }
    ) {
        Text("Submit")
    }
}

Here, padding adds space inside the button's bounds, and size sets dimensions. Modifier order matters—they're applied left to right, and layout constraints propagate accordingly.

For responsive layouts, combine modifiers with weight and alignment:

kotlin
@Composable
fun ResponsiveRow() {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp),
        horizontalArrangement = Arrangement.spacedBy(8.dp)
    ) {
        Box(
            modifier = Modifier
                .weight(1f)
                .height(100.dp)
                .background(Color.Blue)
        )
        Box(
            modifier = Modifier
                .weight(2f)
                .height(100.dp)
                .background(Color.Green)
        )
    }
}

The first Box takes 1/3 of available width, the second takes 2/3. Responsive design without ConstraintLayout XML.

For interactions, use click modifiers:

kotlin
@Composable
fun InteractiveCard(onTap: () -> Unit) {
    Box(
        modifier = Modifier
            .fillMaxWidth()
            .height(120.dp)
            .background(Color.LightGray)
            .clickable(
                indication = ripple(),
                interactionSource = remember { MutableInteractionSource() }
            ) {
                onTap()
            }
            .padding(16.dp)
    ) {
        Text("Tap me")
    }
}

The clickable modifier handles taps with ripple feedback. In production, we layer modifiers to build polished interactions: shadows, borders, rounded corners, all declaratively.

Common mistakes

Not memoizing expensive computations: If a composable does heavy work (network calls, database queries), it reruns on every recomposition. Use remember to cache results or move work to LaunchedEffect.

kotlin
@Composable
fun DataScreen(userId: String) {
    var userData by remember { mutableStateOf(null) }

    LaunchedEffect(userId) {
        userData = fetchUser(userId)  // Only reruns when userId changes
    }

    if (userData != null) {
        UserCard(user = userData)
    }
}

State placed too high or too low: If state lives in the wrong composable, you'll either have prop-drilling nightmares or lose state when the composable recomposes. Use remember at the minimal scope needed, or lift state to a ViewModel for screen-level data.

Mutating state directly instead of through Compose: Never reassign a remember value directly outside a callback. Always update via setState functions or mutableStateOf.

kotlin
// Wrong
var count = 0  // This won't trigger recomposition
count = 1

// Right
var count by remember { mutableStateOf(0) }
count = 1  // Triggers recomposition

Ignoring modifier order: Placing clickable before padding changes the click area size. Test your modifier chains in previews.

Key takeaways

  • Composable functions declare UI as a pure function of state, replacing imperative view construction and reducing state synchronization bugs.
  • Recomposition automatically updates the UI when state changes; Compose optimizes by recomposing only affected branches.
  • The @Preview annotation lets you iterate on UI instantly in Android Studio without running an emulator, dramatically speeding up design cycles.
  • Modifiers chain together to control layout, sizing, spacing, and interactions declaratively, simplifying responsive design.
  • Use remember and mutableStateOf for local state, and leverage LaunchedEffect for side effects like network calls.
  • In production systems, Compose's predictable data flow has reduced subtle UI bugs and improved team velocity significantly.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles