Back to Notes

Why Your LazyColumn Recomposes Everything: Understanding Item Stability in Compose

By Davide Agostini

Understanding LazyColumn performance is crucial for building smooth Android apps. One common pitfall? Unstable data classes causing unnecessary recompositions.

The Problem: Unstable Items Trigger Full Recomposition

When you pass a List<User> to a LazyColumn, Compose needs to know if items change. If your User data class isn't stable, Compose can't skip unchanged itemsβ€”it recomposes everything.

What Makes a Class Stable?

A class is stable when:

  • It's a data class with only val properties (immutable)
  • All property types are themselves stable (primitives, Strings, stable classes)
  • Compose can guarantee the object won't change between renders

🚨 BAD: Unstable Data Class

// ❌ This causes LazyColumn to recompose ALL items when list changes
data class User(
    val id: Int,
    val name: String,
    val email: String,
    val isActive: Boolean = true
)
 
// Problem: When you add/remove one user, Compose might recompose ALL rows
// because it can't guarantee stability across the list
@Composable
fun UserList(users: List<User>) {
    LazyColumn {
        items(users) { user ->
            UserRow(user)  // ⚠️ Recomposed unnecessarily!
        }
    }
}

βœ… GOOD: Stable & Performant

// βœ… Compose can track items by stable identity
// Use Stable marker or ensure all properties are stable
@Composable
fun UserList(users: List<User>) {
    // Key function helps Compose track item identity
    LazyColumn {
        items(
            items = users,
            key = { user -> user.id  // πŸ‘ˆ Critical for performance!
        }) { user ->
            UserRow(user = user)
        }
    }
}
 
// βœ… Even better: Use stable, immutable collections
// When you need to update the list, create a NEW list
fun refreshUsers(): List<User> {
    return listOf(
        User(1, "Alice", "alice@example.com"),
        User(2, "Bob", "bob@example.com")
        // This new list reference tells Compose something changed
    )
}

Common Stability Gotchas

⚠️ Watch Out for These Types

// ❌ UNSTABLE - MutableList can change anytime
@Composable
fun BadExample(users: MutableList<User>) {
    LazyColumn {
        items(users) { user -> UserRow(user) }
    }
}
 
// ❌ UNSTABLE - Regular Date class
data class Event(
    val name: String,
    val date: Date  // πŸ‘ˆ java.util.Date is NOT stable!
)
 
// βœ… STABLE - Use Long for timestamps instead
data class EventStable(
    val name: String,
    val timestamp: Long  // πŸ‘ˆ Stable primitive type
)
 
// βœ… STABLE - Use kotlinx.datetime.Instant
data class EventModern(
    val name: String,
    val instant: Instant
)

Memory Leaks: Another Performance Killer

LazyColumn items that hold references can cause memory leaks if not handled properly.

// ❌ BAD: Capturing context in lambda
@Composable
fun UserRowBad(user: User) {
    val context = LocalContext.current  // πŸ‘ˆ Captured in closure
 
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable {
                // This lambda captures context β†’ potential leak
                Toast.makeText(context, user.name, Toast.LENGTH_SHORT).show()
            }
    ) {
        Text(text = user.name)
    }
}
 
// βœ… GOOD: Use derivedStateOf or remember for context access
@Composable
fun UserRowGood(user: User) {
    val context = LocalContext.current
 
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable {
                // Context accessed only when clicked
                Toast.makeText(context, user.name, Toast.LENGTH_SHORT).show()
            }
    ) {
        Text(text = user.name)
    }
}
 
// βœ… BETTER: Extract click handler to avoid capturing in composition
@Composable
fun UserRowBetter(user: User, onUserClick: (User) -> Unit) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .clickable { onUserClick(user) }  // πŸ‘ˆ No captured context
    ) {
        Text(text = user.name)
    }
}

The Key Takeaway Box

Key Takeaway

Fix LazyColumn performance by: (1) Adding key parameter to items(), (2) Using immutable/stable data classes, and (3) avoiding captured lambdas. Run ./gradlew assembleDebug --no-daemon && ./gradlew lint to catch issues early. Profile with Layout Inspector to identify recomposition hotspots.