Kotlin
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Null Safety
-
Section 4: Object-Oriented Kotlin
-
Section 5: Functional Kotlin
-
Section 6: Coroutines
-
Section 7: Collections Deep Dive
-
Section 8: Type System Deep Dive
-
Section 9: Interop and Tooling
-
Section 10: Kotlin DSLs and Patterns
-
Section 11: Testing and Quality
-
Section 12: Server-Side Kotlin
-
Section 13: Practical Projects
-
Section 14: Interview Practice
-
Section 15: More Practice Exercises
-
Section 16: More Standard Library
-
Section 17: Multiplatform Kotlin
-
Section 18: kotlin.collections In Depth
-
Section 19: kotlin.text In Depth
-
Section 20: kotlin.ranges and kotlin.sequences
-
Section 21: kotlin.io and File Handling
-
Section 22: kotlinx.coroutines Deep Dive
-
Section 23: kotlin.reflect
-
Section 24: Android Development with Kotlin Overview
-
Section 25: Kotlin Multiplatform Deep Dive
-
Section 26: Kotlin for Backend Deep Dive
-
Section 27: Kotlin Design Patterns
-
Section 28: Advanced Language Features
-
Section 29: More Practice Exercises
-
Section 30: More Interview Practice
-
Section 31: Kotlin Type System Deep Dive
-
Section 32: Kotlin Null Safety Advanced
-
Section 33: Kotlin Testing Deep Dive
-
Section 34: Kotlin Build Tooling Deep Dive
-
Section 35: Kotlin Serialization
-
Section 36: Kotlin Performance Considerations
-
Section 37: Kotlin Native Overview
-
Section 38: Kotlin for Data and Scripting
-
Section 39: More Coroutines Practice
-
Section 40: More Android-Adjacent Patterns
-
Section 41: More Practical Projects
-
Section 42: More Design and Architecture Practice
-
Section 43: Kotlin Language Evolution
-
Section 44: More Interview and Review
-
Section 45: Kotlin Delegation Patterns Deep Dive
-
Section 46: Kotlin Annotations Deep Dive
-
Section 47: Kotlin for Gradle Plugin Development
-
Section 48: Kotlin Concurrency Beyond Coroutines
-
Section 49: Kotlin Compiler Internals
-
Section 50: Real-World Kotlin Case Studies
-
Section 51: Final Practice Projects
-
Section 52: Kotlin for Server-Side Reactive Programming
-
Section 53: More Practice and Drills
-
Section 54: Kotlin Security Practices
111: Jetpack Compose State Management Basics
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
rememberandmutableStateOf. - 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
CartScreenthat holds the state and passes it down to theQuantitySelector.
There are no comments for now.