What You'll Learn
This article explains Compose stability (@stable, @Immutable, skippable judgment, Compose Compiler Report, and performance optimization).
Understanding Stability
Compose skips recomposition if arguments haven't changed. Whether skipping is possible depends on the "stability" of the arguments.
// ✅ Stable (auto-detected): primitives, String, function types
@Composable
fun Greeting(name: String) { // String = stable → skippable
Text("Hello, $name")
}
// ❌ Unstable: List, Map and other collections
@Composable
fun UserList(users: List<User>) { // List = unstable → recomposed every time
LazyColumn {
items(users) { UserItem(it) }
}
}
@Immutable
@Immutable
data class User(
val id: String,
val name: String,
val email: String
)
// If all properties are val and stable types, add @Immutable
// Compose treats this class as guaranteed immutable
@stable
@Stable
class CounterState {
var count by mutableIntStateOf(0)
private set
fun increment() { count++ }
}
// @Stable = "As long as equals() returns the same result for the same instance, skipping is possible"
// Use on classes containing MutableState
Stabilizing Collections
// Method 1: kotlinx.collections.immutable
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.toImmutableList
@Composable
fun UserList(users: ImmutableList<User>) { // ✅ Stable
LazyColumn {
items(users.size) { index -> UserItem(users[index]) }
}
}
// Caller side
val users = usersList.toImmutableList()
// Method 2: Wrapper class
@Immutable
data class UserListWrapper(val users: List<User>)
Compose Compiler Report
// build.gradle.kts
android {
composeCompiler {
reportsDestination = layout.buildDirectory.dir("compose_reports")
metricsDestination = layout.buildDirectory.dir("compose_metrics")
}
}
// Example report output
// app_release-composables.txt
restartable skippable scheme("[androidx.compose.ui.UnitComposable]") fun Greeting(
stable name: String
)
restartable scheme("[androidx.compose.ui.UnitComposable]") fun UserList(
unstable users: List<User> // ← Unstable! Not skippable
)
derivedStateOf
@Composable
fun FilteredList(items: List<Item>, query: String) {
// ❌ Recomputed every time
val filtered = items.filter { it.name.contains(query) }
// ✅ Recomputed only when result changes
val filtered by remember(items, query) {
derivedStateOf { items.filter { it.name.contains(query) } }
}
LazyColumn {
items(filtered) { item -> ItemRow(item) }
}
}
Stabilization with key
@Composable
fun ItemList(items: List<Item>) {
LazyColumn {
items(
items = items,
key = { it.id } // ✅ key guarantees identity → reused
) { item ->
ItemRow(item)
}
}
}
remember + lambda stabilization
// ❌ New lambda instance created every time
@Composable
fun Parent(viewModel: MyViewModel) {
Child(onClick = { viewModel.doSomething() })
}
// ✅ remember stabilizes lambda
@Composable
fun Parent(viewModel: MyViewModel) {
val onClick = remember(viewModel) { { viewModel.doSomething() } }
Child(onClick = onClick)
}
Summary
| Technique | Use Case |
|---|---|
@Immutable |
Immutable data classes |
@Stable |
Classes with MutableState |
ImmutableList |
Stabilize collections |
derivedStateOf |
Optimize derived state |
key |
LazyList item identity |
| Compiler Report | Discover unstable args |
- Use Compose Compiler Report to find unstable arguments
- Mark stability with
@Immutable/@Stable - Stabilize collections with
kotlinx.collections.immutable - Prevent unnecessary recalculation with
derivedStateOf
8 production-ready Android app templates (performance optimized) are available.
Browse templates → Gumroad
Related articles:
- Recomposition debugging
- LazyColumn optimization
- Baseline Profile
Ready-Made Android App Templates
8 production-ready Android app templates with Jetpack Compose, MVVM, Hilt, and Material 3.
Browse templates → Gumroad
Top comments (0)