DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Compose Stability and Recomposition Optimization — @Stable/@Immutable/skippable

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

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

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

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

Compose Compiler Report

// build.gradle.kts
android {
    composeCompiler {
        reportsDestination = layout.buildDirectory.dir("compose_reports")
        metricsDestination = layout.buildDirectory.dir("compose_metrics")
    }
}
Enter fullscreen mode Exit fullscreen mode
// 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
)
Enter fullscreen mode Exit fullscreen mode

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

Stabilization with key

@Composable
fun ItemList(items: List<Item>) {
    LazyColumn {
        items(
            items = items,
            key = { it.id } // ✅ key guarantees identity → reused
        ) { item ->
            ItemRow(item)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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

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 templatesGumroad

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 templatesGumroad

Top comments (0)