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