I think I'm missing a core concept of Jetpack Compose here. I'm running into an issue when I'm trying to change a non-constructor
data class
property
inside of a composable when this composable is part of an observed list.
Does not work: (sadProperty
is not declared in the constructor)
data class IntWrapper(val actualInt: Int = 0) {
var sadProperty: Int = 0
}
@Preview
@Composable
fun test() {
var state by remember { mutableStateOf(listOf(IntWrapper(1), IntWrapper(2), IntWrapper(3),IntWrapper(4)))}
fun onClick(item: IntWrapper) {
val indexOf = state.indexOf(item)
val newState = state.minus(item).toMutableList()
val copy = item.copy()
copy.sadProperty = Random.nextInt()
newState.add(indexOf, copy)
state = newState
}
Column() {
for (item in state) {
Text("ac: ${item.actualInt} sad: ${item.sadProperty}", modifier = Modifier.clickable { onClick(item)})
}
}
}
Works: (actualInt
is declared in the constructor)
data class IntWrapper(var actualInt: Int = 0) {
var sadProperty: Int = 0
}
@Preview
@Composable
fun test() {
var state by remember { mutableStateOf(listOf(IntWrapper(1), IntWrapper(2), IntWrapper(3),IntWrapper(4)))}
fun onClick(item: IntWrapper) {
val indexOf = state.indexOf(item)
val newState = state.minus(item).toMutableList()
val copy = item.copy()
copy.actualInt = Random.nextInt()
newState.add(indexOf, copy)
state = newState
}
Column() {
for (item in state) {
Text("ac: ${item.actualInt} sad: ${item.sadProperty}", modifier = Modifier.clickable { onClick(item)})
}
}
}
Could somebody explain why this happens?
This looks like a question of both Jetpack Compose
and about Kotlin
data class, bare with me, I'll try my best.
Lets start with Kotlin's Data classes first
As per the kotlin docs about Data Class
The compiler automatically derives the following members from all properties declared in the primary constructor:
- equals()/hashCode() pair
- toString() of the form "User(name=John, age=42)"
- componentN() functions corresponding to the properties in their order of declaration.
- copy() .
Your IntWrapper
data class has one Primary Constructor, the parenthesis that follows the class name, and with 1 property declared inside of it.
data class IntWrapper(val actualInt: Int = 0) {
var sadProperty: Int = 0
}
with that, we can say, your IntWrapper
data class has
actualInt
)IntWrapper(actualInt=?)
copy()
functionand based again from the docs:
The compiler only uses the properties defined inside the primary constructor for the automatically generated functions. To exclude a property from the generated implementations, declare it inside the class body:
The equals
will only use/evaluate the property declared from IntWrapper's
primary constructor (i.e actualInt : Int
), and sadProperty
is excluded from it because its in the part of the data class body.
Now consider the following:
val intWrapper1 = IntWrapper(actualInt = 5)
intWrapper1.sadProperty = 5
val intWrapper2 = IntWrapper(actualInt = 5)
intWrapper2.sadProperty = 10
Log.e("AreTheyEqual?", "${intWrapper1 == intWrapper2}")
it prints,
E/AreTheyEqual?: true
because the equality
sees both of the derived properties have the same value 5
, sadProperty
is excluded from this comparison.
val intWrapper1 = IntWrapper(actualInt = 5)
intWrapper1.sadProperty = 5
val intWrapper2 = IntWrapper(actualInt = 10)
intWrapper2.sadProperty = 5
prints,
E/AreTheyEqual?: false
because the generated equals verifies that the generated component (actualInt
) is NOT the same from the two IntWrapper
instance.
Now going to Jetpack Compose
, applying everything we understand with data classes,
The first test
complies with everything about data class
, it creates a new object with a new value and that's what Compose
needs to trigger re-composition
.
The second test
will not trigger re-composition
, Compose
still sees the same IntWrapper
instance because sadProperty
is not part of the generated components that will be used by the data class's equals operation.