Jetpack Compose - Stable vs Unstable Types

Overview

In Jetpack Compose, stability determines whether Compose can reliably detect if a value has changed between recompositions. This directly affects performance by enabling smart recomposition - skipping unnecessary recompositions when inputs haven’t changed.

Stable Types

Definition

A type is stable if Compose can reliably determine whether its value has changed.

Characteristics

  • Immutable: All properties are val and of stable types
  • Observable Mutable: Changes notify Compose (e.g., MutableState)
  • Primitives: Int, String, Boolean, etc.
  • Data classes with only stable properties

Examples

// ✅ Stable - Immutable data class
data class UserProfile(val id: Int, val name: String)
 
// ✅ Stable - MutableState is observable
val counter = mutableStateOf(0)
 
// ✅ Stable - Primitives
val message: String = "Hello"

Unstable Types

Definition

A type is unstable if Compose cannot reliably determine whether its value has changed.

Characteristics

  • Types with var properties (not Compose State)
  • Properties of unstable types
  • Standard collections (List, Map, Set) by default
  • Classes without @Stable annotation that don’t meet stability criteria

Examples

// ❌ Unstable - var property
class MutableUser {
    var name: String = ""
    var age: Int = 0
}
 
// ❌ Unstable - Standard collection
val users: List<User> = listOf(...)
 
// ❌ Unstable - Interface without stability guarantee
interface Repository {
    fun getData(): String
}

Impact on Recomposition

Smart Recomposition Rules

  • All parameters stable + unchanged → Composable is skipped
  • Any parameter unstable → Composable always recomposes
  • Stable parameter changed → Composable recomposes

Performance Example

data class UserProfile(val id: Int, val name: String) // Stable
 
@Composable
fun UserProfileView(profile: UserProfile, modifier: Modifier = Modifier) {
    // Can be skipped if 'profile' unchanged
    Column(modifier) {
        Text("ID: ${profile.id}")
        Text("Name: ${profile.name}")
    }
}
 
@Composable
fun ParentScreen() {
    var user by remember { mutableStateOf(UserProfile(1, "Alice")) }
    var counter by remember { mutableStateOf(0) }
 
    Column {
        UserProfileView(profile = user) // ✅ Can be skipped
        Text("Counter: $counter")
        Button(onClick = { counter++ }) {
            Text("Increment Counter")
        }
    }
}

When counter changes, UserProfileView is skipped because user hasn’t changed.

Making Types Stable

1. Use @Stable Annotation

@Stable
class StableRepository(private val api: ApiService) {
    // Compose trusts this is stable
    fun getData(): Flow<String> = api.getData()
}

2. Use @Immutable Annotation

@Immutable
data class Theme(
    val primaryColor: Color,
    val secondaryColor: Color
)

3. Use Immutable Collections

// ✅ Stable with kotlinx.collections.immutable
val users: ImmutableList<User> = persistentListOf(...)
 
// ✅ Or wrap in stable container
@Immutable
data class UserList(val users: List<User>)

4. Convert Mutable to State

// ❌ Unstable
class UserViewModel {
    var users: List<User> = emptyList()
}
 
// ✅ Stable
class UserViewModel {
    var users by mutableStateOf<List<User>>(emptyList())
    // or
    private val _users = mutableStateOf<List<User>>(emptyList())
    val users: State<List<User>> = _users
}

Best Practices

Do

  • Use immutable data classes for UI models
  • Prefer State for mutable values
  • Use @Stable/@Immutable annotations when appropriate
  • Use immutable collections (kotlinx.collections.immutable)

Don’t

  • Pass mutable objects with var properties
  • Use standard collections directly as parameters
  • Ignore stability warnings in IDE/compiler

Performance Impact

With Unstable Types

class MutableUser(var name: String) // Unstable
 
@Composable
fun UserCard(user: MutableUser) {
    // Always recomposes, even if name didn't change
    Card {
        Text(user.name)
    }
}

With Stable Types

data class User(val name: String) // Stable
 
@Composable
fun UserCard(user: User) {
    // Recomposes only when user actually changes
    Card {
        Text(user.name)
    }
}

Key Takeaways

  • Stability enables smart recomposition - the foundation of Compose performance
  • Immutable data structures are naturally stable
  • MutableState provides observable mutability
  • Standard collections are unstable by default
  • Use stability annotations to help Compose’s analysis
  • Unstable parameters force recomposition regardless of actual changes