https://kotlinlang.org logo
#compose
Title
# compose
f

fengdai

08/31/2020, 3:02 AM
How to understand the first term of
@Stable
’s guarantees? > The result of equals will always return the same result for the same two instances.
z

Zach Klippenstein (he/him) [MOD]

08/31/2020, 2:37 PM
I would interpret that to mean that they want to be able to use referential equality checks instead of structural ones. So
===
and
==
must effectively return the same results for any two
@Stable
objects.
a

Adam Powell

08/31/2020, 3:50 PM
It has to do with how we compute skipping and restarting behavior. Very roughly speaking, if something is
@Stable
, then compose can declare that
.equals
indicates one instance is as good as another and that they are equal in how they will change and notify compose of any future changes.
Consider values of a
@Stable
type
a
and
b
. If you call
Copy code
val param = if (something) a else b
MyComposable(param)
then this behavior becomes relevant.
If
MyComposable
is initially called with
a
, and the caller recomposes and it is called with
a
again, we compare
a == a
, and determine we can skip the call to
MyComposable
.
If
a
internally changes, (e.g. a field backed by
mutableStateOf
changes) the bits inside
MyComposable
that read those state values will run again - it's an internal lambda capture of the parameters from the last time
MyComposable
ran without skipping.
so
MyComposable
will run again with
a
as the captured parameter, without recomposing the caller.
This becomes important if
a == b
. The stable contract says one equal instance is as good as another and we can skip the call.
but if we call
MyComposable(b)
, the internal restart capture still points to
a
. If
b
changes and
a
doesn't reflect those same changes over time,
MyComposable
won't recompose.
It means that if
a == b
their future mutation destinies must also be linked.
Practically speaking, this means you should be really careful if you implement
.equals
for a
@Stable
mutable type.
🤯 1
But this isn't as new as it sounds in terms of real-world risk and isn't unique to compose. Implementing `.equals`/`.hashCode` for a mutable type has always been dangerous. Consider what happens if you change an object's hashCode while it is already a key in a live hash table.
This is also why declaring
var
data class constructor properties is a bad idea - it makes that object really dangerous to use as a key in hash tables.
f

fengdai

09/01/2020, 1:28 AM
@Adam Powell Thank you for your explanation. It’s very clear! 🤯
👍 1
Hi @Adam Powell. I see MutableState is annotated with
@Stable
. And I think
a
does not equal
b
in this case:
Copy code
val initialValue = ""
val a = mutableStateOf(initialValue)
val b = mutableStateOf(initialValue)
Even they hold the same initial value but they can be mutated respectively later, which can make them not equal anymore. (Am I right?) So in which case can two instances of MutableState be equal?
f

fengdai

09/01/2020, 7:21 AM
Hi @Adam Powell. I see
androidx.compose.material.Colors
is annotated with
@Stable
. And it's mutable, also implements
.equals
. As you mentioned above, if two instances of Colors
a == b
then
a
will be treated as the same as
b
. But how can it comply with the contract when
a
is changed? Is there some underlying mechanism of Compose that will build a link between
a
and
b
?
I found that below code can break the contract (I will never write code like this in the real-world):
Copy code
val darkColors = darkColors()
val lightColors = lightColors()

@Composable
fun UnstableColors() {
    val a = lightColors
    var b: Colors? by remember { mutableStateOf(null) }
    var darkTheme by remember { mutableStateOf(false) }
    Column {
        MaterialTheme(
            colors = if (darkTheme) darkColors else lightColors,
        ) {
            if (b == null) {
                b = MaterialTheme.colors
            }
            Surface {
                Column {
                    Text(text = if (darkTheme) "darkTheme" else "lightTheme")
                    Text(text = "a hash: ${a.identityHashCode()}")
                    Text(text = "b hash: ${b.identityHashCode()}")
                    Text(text = "a ${if (a == b) "==" else "!="} b")
                }
            }
        }
        Surface {
            Switch(checked = darkTheme, onCheckedChange = { darkTheme = it })
        }
    }
}
Oh, I see.
Colors
is designed this way for performance consideration. But will the break of contract cause unexpected behavior when it’s misused by us?
a

Adam Powell

09/01/2020, 2:42 PM
I think you may have found a bug in
Colors
- thank you 🙂 I'll follow up with the team
👍🏻 2
4 Views