Question

Unable to create updated UI with new image in JetPack Compose

I'm trying to create simple Dice Roller JetPack Compose App. I create an Image compose and button compose. Whenever I click on the button, log statement shows button is clicked but I'm unable to update the dice image according to that data. I'll attach my code for future references. What am I doing wrong?

package com.example.mydiceroller

import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.lifecycle.ViewModelProvider

class MainActivity : ComponentActivity() {

    lateinit var mainViewModel: MainViewModel
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {

            mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
            Column(
                modifier = Modifier
                    .fillMaxSize(),
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.Center
            ) {
                DiceImage(count = mainViewModel.count)
                Button(onClick = {
                    mainViewModel.rollDice()
                }
                )
                {
                    Text(text = "Let's Roll")
                }

            }
        }
    }

}

@Composable
fun DiceImage(count: Int) {


    Log.d("countValue", "${count}")

    var countId=when (count) {
        1 -> R.drawable.dice_1
        2 -> R.drawable.dice_2
        3 -> R.drawable.dice_3
        4 -> R.drawable.dice_4
        5 -> R.drawable.dice_5
        else -> R.drawable.dice_6
    }
    Image(
        painter = painterResource(id = countId), contentDescription = ""
    )
}

MainViewModel.kt

package com.example.mydiceroller

import android.util.Log
import androidx.lifecycle.ViewModel
import java.util.Random

class MainViewModel : ViewModel() {
    var count: Int = 1

    fun rollDice(): Int {
        count = (Random().nextInt(6) + 1)
        Log.d("ViewModel", "ButtonClicked")
        return count
    }
}

I also tried mutableStateOf but failed

 2  40  2
1 Jan 1970

Solution

 1

The problem is that your Compose UI cannot detect that count changed in the view model.

You need to store count in a State object. In a composable you would use something like this (by unwraps the State so it is an Int and can be more easily used):

var count: Int by remember { mutableStateOf(1) }

Whenever this variable is updated a recomposition is triggered and the UI is updated accodingly. Since you use a view model to store count you need to employ a Flow:

class MainViewModel : ViewModel() {
    private val _count = MutableStateFlow(1)
    val count = _count.asStateFlow()

    fun rollDice() {
        Log.d("ViewModel", "ButtonClicked")
        _count.value = (1..6).random()
    }
}

Whenever rollDice is called the count flow changes. You can convert this flow in your composables into a Compose State object with collectAsStateWithLifecycle (You need the gradle dependency androidx.lifecycle:lifecycle-runtime-compose):

setContent {
    val mainViewModel: MainViewModel = viewModel()
    val count: Int by mainViewModel.count.collectAsStateWithLifecycle()

    Column(
        modifier = Modifier
            .fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {
        DiceImage(count = count)

        Button(onClick = {
            mainViewModel.rollDice()
        }) {
            Text(text = "Let's Roll")
        }
    }
}
2024-07-19
Leviathan

Solution

 0

Thanks @Leviathan for your solution. I'm revising the @leviathan solution which works for me

setContent {
//    mainViewModel = MainViewModel()
//    mainViewModel = ViewModelProvider(this).get(MainViewModel::class.java)
//    val count = mainViewModel.count.collectAsStateWithLifecycle()
    val state = remember {
        MainViewModel()
    }
    val uiState by state.count.collectAsStateWithLifecycle()
    Log.d("DiceRollerMainActivity", "State Flow Count Value ${uiState}")

    val imageresource = when (uiState) {
        1 -> R.drawable.dice_1
        2 -> R.drawable.dice_2
        3 -> R.drawable.dice_3
        4 -> R.drawable.dice_4
        5 -> R.drawable.dice_5
        else -> R.drawable.dice_6
    }
    Log.d("DiceRollerMainActivity", "Image Resource Value ${imageresource}")

    Column(
        modifier = Modifier
            .fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
    ) {
        Image(
            painter = painterResource(id = imageresource),
            contentDescription = ""
        )

        Button(onClick = {
            state.rollDice()
        }) {
            Text(text = "Let's Roll")
        }
    }
}
2024-07-20
Aamir Khan