2

I'm working with Compose and the ViewModel and a list. I don't understand why my UI isn't recomposing when I click on the button, although I am generating a new list.

data class Dice(var value: Int = 6) {
    fun roll() {
        value = Random.nextInt(6) + 1
    }
}

class MainViewModel : ViewModel() {
    var diceVal = MutableStateFlow(listOf(Dice()))

    fun rollDice() {
        //Both not working
        //diceVal.value = diceVal.value.map { it.roll(); it.copy() }
        diceVal.update { it.map { it.roll(); it.copy() }}
    }
}

@Composable
fun MainScreen(modifier: Modifier = Modifier, viewModel: MainViewModel = viewModel { MainViewModel() }) {
    Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) {
        var diceList = viewModel.diceVal.collectAsStateWithLifecycle()

        diceList.value.forEach {
            Button(onClick = {
                viewModel.rollDice()
            }) {
                Text(text = it.value.toString())
            }
        }
    }
}
2
  • try using diceVal.value = listOf(diceVal.value.map { it.roll(); it.copy() }) basically you need to give it a new list instead of using the one thats in there Commented Oct 24 at 16:40
  • @tyczj The list was created by listOf() and isn't modifyable. By calling map on it, a new list is created - that's not the problem here. Instead the Dice value being declared as var is the culprit, see my answer for an explanation why that matters and how to solve it. Commented Oct 24 at 16:57

1 Answer 1

3

You cannot have mutable objects inside of Compose State, the Compose runtime cannot detect any modifications to it and won't automatically update the UI.

Specifically, the value property of Dice must be declared as val, not as var.

Now, you cannot modify the dice value by calling roll() anymore, but you can rewrite the function to create a new Dice instead. That could look something like this:

data class Dice(val value: Int = 6) {
    companion object {
        fun roll(): Dice = Dice(
            value = Random.nextInt(6) + 1,
        )
    }
}

The roll function is now moved to the companion object, which means that you can call it on the class, not on the object of Dice, like this: Dice.roll()

And the view model can then simply roll a new Dice instead of trying to modify the existing ones:

class MainViewModel : ViewModel() {
    private val _diceVal = MutableStateFlow(listOf(Dice()))
    val diceVal = _diceVal.asStateFlow()

    fun rollDice() {
        _diceVal.update { it.map { Dice.roll() } }
    }
}

Please note that I made the MutableStateFlow private, it should only be modifyable from the view model, not from the outside. Instead a read-only StateFlow is created with asStateFlow(). The MutableStateFlow is also declared as val here because you only want to mutate the content of the flow, you never want to change _diceVal for another flow. The same applies to diceList in your Composable, it should be val as well.

And lastly, the Kotlin way to create a random number between 1 and 6 would idiomatically be written like this:

(1..6).random()

Works the same, but it is easier to read and it's immune to off-by-one errors.

Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.