When building modern UI with Jetpack Compose, understanding the nuances between @Immutable and @Stable annotations can significantly affect your app’s performance and stability. While both annotations serve different purposes, they work together to help Compose efficiently manage recompositions.
What is @Immutable?
The @Immutable annotation marks objects whose properties cannot be modified after initialization, ensuring that the object’s state remains unchanged. However, this does not guarantee that the object’s state will remain constant throughout the lifetime of the application, but rather that the object itself is immutable and its methods are referentially transparent. This is particularly useful for data classes or objects that don’t hold any mutable state.
data class Profile(
val name: String,
val age: Int
)
In this example, the Profile class is immutable, meaning Compose can skip recompositions when the object itself doesn’t change. However, Compose still observes changes in the UI tree and determines whether a recompose is needed based on whether the Profile object itself is updated.
When to Use @Immutable?
You should use @Immutable when dealing with data that remains constant, such as user details, configuration settings, or any static data that won’t change after initialization. By marking these objects as immutable, you allow Compose to optimize its rendering and avoid redundant recompositions.
Learn more about managing immutability in data classes on Google’s Kotlin Style Guide.
What is @Stable?
The @Stable annotation marks objects that can change internally, but Compose is aware of these changes. When a property of a @Stable object changes, Compose will recompose the affected UI elements accordingly, ensuring that only necessary recompositions are triggered.
@Stable
class UserSettings {
var theme: MutableState<String> = mutableStateOf("Light")
private set
fun changeTheme(newTheme: String) {
theme.value = newTheme
}
}
In this example, the UserSettings class can change its internal theme property, and if this change affects the UI, Compose will recompose the affected parts of the UI. Compose will only recompose where necessary, but it will still observe changes to mutable properties within @Stable objects. This ensures efficient rendering while maintaining flexibility in the object’s internal state.
When to Use @Stable?
Use @Stable for objects that may change internally, and Compose will monitor those changes. If changes affect the UI, Compose will trigger recompositions where necessary. This ensures efficient updates without unnecessary recompositions, even when the object’s internal state is mutable., such as preferences, settings, or internal component states. It helps Compose determine when an object has been updated in a meaningful way.
Explore the power of stable state management in Compose on Jetpack Compose State Documentation.
Best Practices for @Immutable and @Stable
Choosing between @Immutable and @Stable boils down to how your objects are structured and how you intend them to behave in your UI:
- Use @Immutable for truly static data that never changes after it’s created.
- Use @Stable for objects that may change internally, but not in a way that affects the UI’s rendering efficiency.
By applying these annotations correctly, you can drastically reduce unnecessary recompositions in Compose, leading to a smoother and more performant UI.
Examples in Action
Here’s a quick example to see how @Immutable and @Stable work together to optimize UI performance in Jetpack Compose:
@Composable
fun UserProfile(profile: Profile) {
Text(text = "Name: ${profile.name}")
Text(text = "Age: ${profile.age}")
}
@Composable
fun SettingsScreen(userSettings: UserSettings) {
Button(onClick = { userSettings.changeTheme("Dark") }) {
Text("Change Theme")
}
}
In this example, the UserProfile composable will not recompose unless the Profile object itself changes (since it is marked with @Immutable), while the SettingsScreen composable can recompose when the changeTheme method is called in UserSettings, which is marked as @Stable.
Conclusion
Understanding and properly using @Immutable and @Stable annotations is crucial for optimizing performance in Jetpack Compose applications. By leveraging these annotations, you can ensure that your app runs smoothly and avoids unnecessary UI updates.
For more in-depth guidance, check out the Compose Performance Documentation.
Did you like this article?
You can subscribe to my newsletter below and get updates about my new articles.
“The @Stable annotation, on the other hand, is used for objects that might have mutable properties but will not trigger recompositions unnecessarily.
while the SettingsScreen composable can recompose when the changeTheme method is called in UserSettings, which is marked as @Stable.”
The concept of @Stable still sounds confusing. The change to UserSettings leads to SettingsScreen recomposition. The statement above says explicitly that it will not trigger recomposition unnecessarily. I can only assume that if the same instance of UserSettings passed to compostable function(e.g. from another parent compostable) the SettingsScreen won’t recompose, but not until you mutate state of the UserSettings, is that correct? How does compose runtime recognizes change of @Stable marked mutable objects?
Yes, that’s right, since we use @Stable, the parent composable is not redrawn, only the views where UserSetting is used are recreated in the current composable. eg;
@ Compasable
HomeScreen(){
val userSettings by remember { mutableStateOf(UserSettings())}
userSettings.notificationsEnabled = false // REACTION1
…many ui elements
//TOP COMPOSABLES ARE HERE
SettingsScreen(userSettings = userSettings){
}
}
@ Composable
fun SettingsScreen(userSettings: UserSettings){
Text(“Theme: ${userSettings.theme}”)
// The change made in REACTION1 only recreated the switch
Switch(
checked = userSettings.notificationsEnabled,
onCheckedChange = { newValue -> userSettings.notificationsEnabled = newValue }
)
}
If stable was not used, many of the UI above would be redrawn and cause performance problems.