5 State Management Mistakes That Are Killing Your Jetpack Compose Performance
I keep seeing the same performance mistakes over and over. These aren't syntax errors or crashes. They're silent killers that make your app feel sluggish.
Most developers know about state management. But knowing and doing it right are different things. Here are the mistakes I made and sometimes I'm still making. (The reason why this post is written is to remind myself)
Mistake #1: Reading State in the Wrong Place
This one trips up everyone. You put state at the top of your composable, then wonder why everything recomposes.
@Composable
fun UserProfile(userId: String) {
val user by viewModel.userState.collectAsState()
val posts by viewModel.postsState.collectAsState()
Column {
ProfileHeader(user) // Recomposes when posts change
PostsList(posts) // Recomposes when user changes
}
}
The fix? Move state reads closer to where you use them.
@Composable
fun UserProfile(userId: String) {
Column {
ProfileHeader {
val user by viewModel.userState.collectAsState()
UserHeaderContent(user)
}
PostsList {
val posts by viewModel.postsState.collectAsState()
PostsContent(posts)
}
}
}
Now each section only recomposes when its own data changes.
Mistake #2: Forgetting About Stability
Compose has this thing called "stability." If your data isn't stable, Compose assumes it might change and skips important optimizations.
data class UserProfile(
val name: String,
val avatar: String,
val preferences: MutableMap<String, Any> // This kills stability
)
@Composable
fun ProfileCard(profile: UserProfile) {
// This will recompose way more than needed
Card {
Text(profile.name)
AsyncImage(url = profile.avatar)
}
}
The MutableMap
makes the whole class unstable. Here's the fix:
@Immutable
data class UserProfile(
val name: String,
val avatar: String,
val preferences: Map<String, String> // Immutable now
)
Or use the @Stable
annotation if you need some mutability:
@Stable
class UserProfileState {
var name: String by mutableStateOf("")
var avatar: String by mutableStateOf("")
val preferences: Map<String, String> = emptyMap()
}
Mistake #3: Creating State in Composables
I see this everywhere. People create ViewModels or state holders inside composables.
@Composable
fun ProductList() {
val viewModel = ProductViewModel() // New instance every recomposition
val products by viewModel.products.collectAsState()
LazyColumn {
items(products) { product ->
ProductItem(product)
}
}
}
Every recomposition creates a new ViewModel. Your data gets reset. Your app feels broken.
Use hiltViewModel()
or viewModel()
instead:
@Composable
fun ProductList(
viewModel: ProductViewModel = hiltViewModel()
) {
val products by viewModel.products.collectAsState()
LazyColumn {
items(products) { product ->
ProductItem(product)
}
}
}
Mistake #4: Heavy Calculations in Composable Bodies
Composables can run on every frame during animations. Don't do expensive work in them.
@Composable
fun ExpensiveCalculation(data: List<DataPoint>) {
val result = data
.filter { it.isValid() }
.groupBy { it.category }
.mapValues { it.value.sumOf { point -> point.value } } // Runs every recomposition
DisplayResult(result)
}
Use remember
to cache expensive calculations:
@Composable
fun ExpensiveCalculation(data: List<DataPoint>) {
val result = remember(data) {
data
.filter { it.isValid() }
.groupBy { it.category }
.mapValues { it.value.sumOf { point -> point.value } }
}
DisplayResult(result)
}
The calculation only runs when data
actually changes.
Mistake #5: Not Using Derivations
Sometimes you need computed state based on other state. Don't create separate state for this.
@Composable
fun ShoppingCart() {
var items by remember { mutableStateOf(emptyList<CartItem>()) }
var total by remember { mutableStateOf(0.0) } // Separate state for total
// Manually updating total everywhere
Button(
onClick = {
items = items + newItem
total = items.sumOf { it.price } // Easy to forget
}
) {
Text("Add Item")
}
}
Use derivedStateOf
instead:
@Composable
fun ShoppingCart() {
var items by remember { mutableStateOf(emptyList<CartItem>()) }
val total by remember {
derivedStateOf { items.sumOf { it.price } }
}
Button(
onClick = { items = items + newItem }
) {
Text("Add Item")
}
}
Total updates automatically. No manual syncing needed.
The Real Impact
These mistakes don't just slow down your app. They make development harder too. When everything recomposes, debugging becomes a nightmare. Performance tools show red everywhere. Users complain about battery drain.
I learned this stuff the hard way. My first Compose app was a disaster. Everything lagged. The team spent weeks hunting performance issues. Turns out, it was all state management.
What's Next?
Start with one mistake at a time. Don't try to fix everything at once. I usually start with stability annotations. They're easy to add and give immediate benefits.
Use the Compose compiler reports to see what's actually stable in your code. Add this to your build file:
android {
compileOptions {
freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${project.buildDir.absolutePath}/compose_compiler"
)
}
}
Run a build and check the reports. You'll be surprised what's unstable.
Performance in Compose isn't magic. It's about understanding how recomposition works and writing code that works with it, not against it. These five mistakes are where most problems start. Fix them, and your app will feel completely different.
Top comments (0)