DEV Community

boryanz
boryanz

Posted on

Five Jetpack Compose state management mistakes I made

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
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
)
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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")
    }
}
Enter fullscreen mode Exit fullscreen mode

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"
        )
    }
}
Enter fullscreen mode Exit fullscreen mode

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)