Hey, I’ve spend the last couple of hours figuring ...
# compose
l
Hey, I’ve spend the last couple of hours figuring out when
@Immutable
and
@Stable
should really be used to make the most out of them. I couldn’t really find any use cases for them, as even marking a class with
@Immutable
annotation doesn’t mean it will always be skipped from recomposition after being altered (code in the thread). Does anyone have any code examples showing benefits of using either one?
Copy code
@Immutable
data class MyStateClass(
    val foo: MutableState<String> = mutableStateOf("foo"),
    var bar: String = "bar",
)

@Composable
fun Start() {
    val foobar = remember { MyStateClass() }

    Column {
        FooBar(foobar)
    }

    LaunchedEffect(Unit) {
        delay(1000)
        foobar.foo.value = "bar"
        foobar.bar = "foo"
    }
}

@Composable
fun FooBar(name: MyStateClass) {
    LogCompositions("Foobar")
    Text(text = name.foo.value)
    Text(text = name.bar)
}
When we call
Start()
composable, it then launches a delayed LaunchedEffect that modifies our “immutable” object.
LogCompositions
notifies me about 2 compositions that have happened, whereas I would expect just one
@Adam Powell I saw a couple of your answers to similar questions in this channel, none of which really helped me understand how those annotations are meant to work in real world - do I get something wrong here?
s
I think MyStateClass should be @Stable not @Immutable , because foo is mutable
1
l
Yeah, that’s just an example showing that those annotations are not really taken into account by the compose compiler
l
I am not 100% sure, but marking something @Immutable doesn’t disable the usual recomposition checks if the value changes. It only enables optimizations when the instance is the same, not when the instance changes
I don’t know the algorithm, but I assume even if you pass the same values to a composable, some checks are performed anyway to make sure you don’t need to recompose. The @Immutable allows you to tell compose to not worry about it
But in your case, you are changing the properties of foo, which means the equals function result changes, and compose can detect that
@Immutable usage is to help the compiler when you are certain that a thing is immutable, its not to trick the compiler into skipping stuff
a
@Immutable
and
@Stable
do exactly the same thing today.
@Immutable
wouldn't exist as a Compose annotation except for it being an easier concept to explain to people than
@Stable
🙂 if Kotlin had an
immutable
soft keyword or something we'd key off of that for the same optimizations.
collectively both of them are "stable markers" - the compose compiler plugin can infer them in many cases, so if you go back ~6 months or so you'll see a lot more discussion about explicitly annotating things as
@Immutable
or
@Stable
that is less relevant today
if something is stable to compose, it means that if two objects are equal they will always be equal, (they cannot change independent of one another,) and that if they change, they will notify compose of those changes.
MyStateClass
is broken in two ways as declared at the beginning of the thread: it is a
data class
that declares a
var
constructor parameter (which arguably should be a compiler error for Kotlin in general since it leads to a whole lot of undesirable behavior in a system, e.g. if you ever use one as a hash table key) and it is declared as
@Immutable
when it is neither immutable nor stable.
generally you don't want to mix snapshot-mutable and non-snapshot-mutable elements in a class.
MyStateClass
would therefore better be declared as
Copy code
class MyStateClass(
  foo: String = "foo",
  bar: String = "bar"
) {
  var foo by mutableStateOf(foo)
  var bar by mutableStateOf(bar)
}
and the stability should be inferred by the compose compiler plugin.
👍 1
in particular, this means that two different
MyStateClass
instances
a
and
b
are not
.equals
to one another;
a != b
, and this is correct. If
a.foo
changed,
b.foo
would not reflect that change, meaning that
a
and
b
are not stably equal.
The reason this matters to Compose is that Compose will skip a
@Composable
function call if its stable parameters are equal to one another, because stably equal means that for all practical purposes,
a
is as good (and correct) as
b
. But if
a
and
b
are capable of changing independent of one another,
a
is not as good as
b
and that
@Composable
function call cannot skip and still be correct - it could be listening for changes to an old/stale object that will never come, but the newer object may continue to change. It must not skip that call because it needs to update and listen to the new object instead of the old.
s
Wow, Great insight once again 👏 @Adam Powell I really wish if in the future you decide to write a book 😃 or even blog posts about Compose or software in general it would be a really useful resource to learn from.
😅 1
🙏 1
l
Thank you @Adam Powell, this is a super clear explanation. The compose documentation needs a “History” section for some of these pages 😅
👍 1
l
Haha I totally agree with Luis! I knew the model was broken, that was the whole point behind this experiment 😛 Thank you Adam for the response, definitely clarifying some bits around how these annotations work