What do the @Stable and @Immutable annotations mean in Jetpack Compose?
Asked Answered
M

2

58

While studying through the Jetpack Compose sample project, I saw @Stable and @Immutable annotations. I've been looking through the Android documentation and GitHub about those annotations, but I don't understand.

From what I understand, if use @Immutable, even if the state is changed, recomposition should not occur. However, as a result of the test, recomposition proceeds.

What exactly do @Stable and @Immutable annotations do in Jetpack Compose?

Monochromatism answered 29/7, 2021 at 12:28 Comment(0)
S
93

The definition

@Immutable is an annotation to tell Compose compiler that this object is immutable for optimization, so without using it, there will be unnecessary re-composition that might get triggered.

@Stable is another annotation to tell the Compose compiler that this object might change, but when it changes, Compose runtime will be notified.

It might not make sense if you read up to here. So more explanation...


The Compose metrics report

When you generate the compose metrics report, it will mark things as stable or unstable, for unstable objects, Compose compiler cannot tell if the object is modified, so it has to trigger recomposition regardlessly. Here's two snippets of how the report looks like:

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun SomeClass1(
  stable modifier: Modifier? = @static Companion
)

restartable scheme("[androidx.compose.ui.UiComposable]") fun SomeClass2(
  stable modifier: Modifier? = @static Companion
  stable title: String
  unstable list: List<User>
  stable onClicked: Function1<User>, Unit>
)

skippable is desired!

In the case of SomeClass1, it is marked as skippable, because all of it's parameters are marked stable. For SomeClass2, it doesn't get marked as skippable, because it has a property list that is unstable.

When it's marked as skippable, it is a good thing, because Compose compiler can skip recomposition whenever possible and it's more optimized.

when will it fail to be marked as skippable?

Usually compose compiler is smart enough to deduce what is stable and what is unstable. In the cases where compose compiler cannot tell the stability are mutable objects, e.g. a class that contains var properties.

class SomeViewState {
  var isLoading: Boolean
}

Another case where it will fail to decide the stability would be for classes like Collection, such as List, because even the interface is List which looks immutable, it can actually be a mutable list. Example:

data class SomeViewState {
    val list: List<String>
}
@Composable
fun ShowSomething(data: SomeViewState) {
}

Even though the Composable above accepts SomeViewState where all it's property is val, it is still unstable. You might wonder why? That's because on the use side, you can actually use it with a MutableList, like this:

ShowSomething(SomeViewState(mutableListOf()))

For this reason, the compiler will have to mark this as unstable.

So in cases like this, what we want to achieve is to make them stable again, so they are optimized.


@Stable and @Immutable

There are 2 ways to make it stable again, which are using @Stable and @Immutable.

Using @Stable, as mentioned above, it means that the value can be changed, but when it does change, we have to notify Compose compiler. The way to do it is through using mutableStateOf():

@Stable
class SomeViewState {
  var isLoading by mutableStateOf(false)
}

Using @Immutable, it means that you will always make a new copy of the data when you pass into the Composable, in other wards, you make a promise that your data is immutable. From the example above:

@Immutable
data class SomeViewState {
    val list: List<String>
}
@Composable
fun ShowSomething(data: SomeViewState) {
}

After annotating with @Immutable, on your use side, you should make sure to make a new list instead of mutating your list directly.

Example DO:

class ViewModel {
    val state: SomeViewState = SomeViewState(listOf())
    fun removeLastItem() {
        val newList = state.list.toMutableList().apply {
                removeLast()
            }
        state = state.copy(
            list = newList
        )
    }
}

Example DON'T:

class ViewModel {
    val state: SomeViewState = SomeViewState(mutableListOf())
    fun removeLastItem() {
        state.list.removeLast() // <=== you violate your promise of @Immutable!
    }
}

For deeper understanding, you can read this links:

Stieglitz answered 15/11, 2022 at 6:52 Comment(4)
This is a perfect explanation. – Chaker
A way better (or actual deeper) explanation than the accepted answer. Nice job! Thank you πŸ‘. – Fomalhaut
In the @Immutable SomeViewState sample you can also use Kotlin's official Immutable Collections Library (alpha) PersistentList implementation, e.g. val list: PersistentList<String> = persistentListOf(). That way you wouldn't need to provide an own list modification implementation in your ViewModel as PersistentList takes care of the copying (and can even share the underlying data structure for added efficiency). – Allotropy
might have gotten that link from Chris Banes wrong fyi - chrisbanes.me/posts/composable-metrics – Boatbill
S
48

The compiler treats both identically but

  • using @Immutable is a promise that the value will never change.
  • using @Stable is a promise that the value is observable and if it does change listeners are notified.
Sandry answered 29/7, 2021 at 12:54 Comment(8)
what's the advantage of annotating a class with these? – Corvese
@JimOvejera - They let the compose compiler do some optimizations, resulting in smaller and faster recomposition logic. – Alonaalone
can you give us some custom implementation of @Stable ? (other than compose builtins like Color etc) – Bethezel
When using @Stable, it's necessary to use mutableStateOf? – Endothelioma
@TedHopp "some optimizations" like what? Can you add reference? – Un
@Un - Composition skips composables when state hasn't changed. Sometimes it can infer that things haven't changed on its own. The annotations @Immutable and @Stable help it do so for objects that might otherwise trigger recomposition. See, for instance, medium.com/androiddevelopers/… or jetpackcompose.app/articles/…. – Alonaalone
@TedHopp Thank you, now that makes total sense – Un
Why, for example, is Color marked as @Stable instead of @Immutable? – Heracliteanism

© 2022 - 2025 β€” McMap. All rights reserved.