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 classwith onlyvalproperties (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.