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