Home > database >  Jetpack compose mutableStateOf list doesn't trigger re-composition when changing property value
Jetpack compose mutableStateOf list doesn't trigger re-composition when changing property value

Time:10-29

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?

CodePudding user response:

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

  • 1 component (actualInt)
  • toString() of the form IntWrapper(actualInt=?)
  • a generated copy() function
  • a generated equals()/hashCode() pair

and 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.

CodePudding user response:

In Compose, you must use one of the following two methods to perform the recomposition operation successfully:

1 - using mutableStateListOf(), however, by updating the value of the item in the list, the recomposition operation is performed

2- Using your own method that you posted

But for the second method, you need to tell Compose that actualInt has changed, so you need to create a new instance of int.

If you don't want to do this, you need to explain your scenario more so that I can provide a more complete guide

  • Related