Skip to Content
Course content

111: Jetpack Compose State Management Basics

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact moment happen to almost every developer transitioning from traditional Android XML views to Compose. You write a function, you declare a variable, you update that variable in a click listener, and then you stare at the screen wondering why absolutely nothing is happening. It's frustrating because, logically, the variable is changing. If you put a println() in there, you'll see the value incrementing in the logs, but the UI stays frozen.

The "Just use a var" Fallacy

The mistake is thinking that a standard Kotlin variable inside a Composable function is the same thing as a "property" in a traditional Class. In Compose, your functions are just that—functions. They get called over and over again whenever the UI needs to refresh. This is called recomposition.

Look at this broken example. I've written this mistake myself more times than I'd like to admit:

@Composable
fun LikeButton() {
    var likes = 0 // This is the problem

    Button(onClick = { likes++ }) {
        Text("Likes: $likes")
    }
}

Here is why this fails twice over: First, Compose has no way of knowing that likes changed. It's just a plain integer; it doesn't "broadcast" changes to the framework. Second, even if Compose did trigger a redraw, it would run the LikeButton function from the top again. That means likes gets reset to 0 every single time the UI refreshes. You're effectively fighting a battle against the function's own lifecycle.

Giving the UI a Memory with State and Remember

To fix this, we need two things: a way to tell Compose "watch this value," and a way to tell Compose "don't reset this value when you redraw." This is where mutableStateOf and remember come in.

mutableStateOf creates a MutableState object. When the value inside it changes, Compose automatically schedules a recomposition for any Composable that reads that value. However, if we just used mutableStateOf alone, it would still be recreated every time the function runs. That's why we wrap it in remember, which essentially tells Compose to store that value in its internal "memo" and reuse it across recompositions.

import androidx.compose.runtime.*

@Composable
fun LikeButton() {
    // Use 'by' for property delegation so we can treat 'likes' like an Int
    var likes by remember { mutableStateOf(0) }

    Button(onClick = { likes++ }) {
        Text("Likes: $likes")
    }
}

I highly recommend using the by keyword (property delegation) instead of the equals sign. It saves you from having to write likes.value everywhere in your code, which makes the logic much cleaner to read.

Lifting State to avoid "Ghost" Data

As you build larger screens, you'll notice that keeping state inside a single Composable (what we call "internal state") becomes a liability. If you have a LikeButton inside a PostCard, and the PostCard needs to know if the post is liked to change its background color, the button can't keep that state to itself.

The solution is "State Hoisting." You move the state up to the parent Composable and pass the value down and the events up. This makes your components "stateless," which makes them incredibly easy to test and preview.

@Composable
fun PostCard() {
    var isLiked by remember { mutableStateOf(false) }

    Column {
        Text("Cool Kotlin Tip!")
        // We pass the state down and the action up
        LikeButton(
            liked = isLiked, 
            onLikeClicked = { isLiked = !isLiked }
        )
    }
}

@Composable
fun LikeButton(liked: Boolean, onLikeClicked: () -> Unit) {
    Button(onClick = onLikeClicked) {
        Text(if (liked) "❤️ Liked" else "🤍 Like")
    }
}

By doing this, the LikeButton doesn't care how the state is managed; it just knows how to look liked and how to tell the parent that it was clicked. This is the fundamental pattern you'll use in almost every professional Compose project.




📋 Practical Task

Build a "Quantity Selector" Component

Create a Composable called QuantitySelector that allows a user to increase or decrease a numeric value (like choosing the number of items to add to a shopping cart).

Requirements:

  • The component should have a "minus" button, a text display of the current count, and a "plus" button.
  • The state must be managed using remember and mutableStateOf.
  • Constraint: The count should never go below 0. You must add logic to the minus button to prevent negative numbers.
  • Apply state hoisting: Create a parent Composable called CartScreen that holds the state and passes it down to the QuantitySelector.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.