Skip to content
Illustration of Jetpack Compose UI performance optimization using @Immutable and @Stable annotations

Differences & Uses Of @Immutable vs @Stable in Jetpack Compose

3 mins2 comments

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.

Table of Contents

Toggle

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 the newsletter to hear about new articles.

Related Posts

Posted ByGörkem Kara

Görkem KARA is a Senior Android Developer with 9 years of experience. It produces innovative mobile solutions with technologies such as Kotlin, Java and Android SDK. He works as a freelancer and provides customized services on performance optimization, user experience design and application architecture in projects.

What did you think of this post?

Comments 2

  1. tom_koptel

    “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?

    1. Görkem KARA

      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.

Leave a comment

Comments appear after the site owner approves them.

Your comment is published without your name; your email is used only for this comment and never published. Privacy notice

Security check Runs automatically when you send

Built for free with heysirket.com · make yours