How to force recomposition of composable in Jetpack Compose? Change color button

278 views Asked by At

I'm new in Android development. Currently I'm trying to do a true or false quizz app. Its main screen presents a sentence and two buttons, True and False, which are white at first. Just one button can be clicked: if it is the right answer, it must immediately change its color to green, if it's the wrong one, it will change to red. Under these True/False buttons there are two more, the navigation buttons, Previous and Next, which allow the user to navigate through different sentences, each with its own answers.

I have already coded the logic part, using printl() to check the if/else behavior. However, the color of the buttons isn't changing properly. Sometimes I have to go to the next sentence and back for the button to get to the right color from white, and some other times colors don't work at all.

I suspect that if recomposition of the composable is forced everything will be solved. Is there a way to force recomposition?


@Composable
fun Options(modifier: Modifier, i: Int) {

    ConstraintLayout(modifier) {
        val (trueButton, falseButton) = createRefs()

        val chain =
            createHorizontalChain(trueButton, falseButton, chainStyle = ChainStyle.SpreadInside)

        OptionButton(modifier = Modifier.constrainAs(trueButton) {
            start.linkTo(parent.start)
            top.linkTo(parent.top)
            end.linkTo(falseButton.start)
            bottom.linkTo(parent.bottom)
        }, text = "TRUE", i, 0)
        OptionButton(modifier = Modifier.constrainAs(falseButton) {
            start.linkTo(trueButton.end)
            top.linkTo(parent.top)
            end.linkTo(parent.end)
            bottom.linkTo(parent.bottom)
        }, text = "FALSE", i, 1)
    }
}

@Composable                                //  THIS IS THE FUNCTION WHERE I HAVE THE PROBLEM
fun OptionButton(modifier: Modifier, text: String, i: Int, order: Int) {

    var color by remember { mutableStateOf(questions[i].colors[order]) }

    /*if (color == Color.Green) println("color is GREEN")
    if (color == Color.Red) println("color is RED")*/

    Button(modifier = modifier
        .height(80.dp)
        .width(160.dp), colors = ButtonDefaults.buttonColors(
        contentColor = Color.Black, containerColor = questions[i].colors[order]
    ), border = BorderStroke(width = 4.dp, color = Color.Black), onClick = {
        if (questions[i].isChecked == false) {                   // SENTENCE HAS NOT BEEN ANSWERED
            if (questions[i].options[order] == questions[i].answer) {
                questions[i].isChecked = true
                println("RIGHT ANSWER")
                questions[i].colors[order] = Color.Green
                color = questions[i].colors[order]


            } else {                                           // SENTENCE HAS BEEN ANSWERED
                questions[i].isChecked = true
                println("WRONG ANSWER")
                questions[i].colors[order] = Color.Red
                color = questions[i].colors[order]
            }
        } else {

        }

    }) {
        Text(text = text, fontSize = 30.sp)
    }
}


data class Question(
    val sentence: String,
    val options: Array<String> = arrayOf("True", "False"),
    var isChecked: Boolean = false,
    var colors: Array<Color> = arrayOf(Color.White, Color.White),
    var answer: String
) {}

val questions = listOf<Question>(
    Question("Elephant is the biggest living land animal", answer = "True"),
    Question("Lions fly", answer = "False"),
    Question("Whale is the biggest living animal", answer = "True"),
    Question("Trees walk", answer = "False"),
    Question("Rhinos are mammals", answer = "True"),
    Question("Snakes are vegetarians", answer = "False")
)

2

There are 2 answers

0
K9994 On

I solved the problem. From official doc "Using mutable objects such as ArrayList or mutableListOf() as state in Compose causes your users to see incorrect or stale data in your app. Mutable objects that are not observable, such as ArrayList or a mutable data class, are not observable by Compose and don't trigger a recomposition when they change. Instead of using non-observable mutable objects, the recommendation is to use an observable data holder such as State<List> and the immutable listOf()."

I was using mutableListOf() instead of mutableStateListOf()

Here the code:

fun OptionButton(modifier: Modifier, text: String, i: Int, order: Int) {


    var color by remember { mutableStateOf(questions[i].colors) }


    /*if (color == Color.Green) println("color is GREEN")
    if (color == Color.Red) println("color is RED")*/

    Button(modifier = modifier
        .height(80.dp)
        .width(160.dp), colors = ButtonDefaults.buttonColors(
        contentColor = Color.Black, containerColor = questions[i].colors[order]
    ), border = BorderStroke(width = 4.dp, color = Color.Black), onClick = {
        if (questions[i].isChecked == false) {                          // SENTENCE HAS NOT BEEN ANSWERED
            if (questions[i].options[order] == questions[i].answer) {
                questions[i].isChecked = true
                println("RIGHT ANSWER")
                questions[i].colors[order] = Color.Green
                color = questions[i].colors


            } else {                                                   // SENTENCE HAS BEEN ANSWERED
                questions[i].isChecked = true
                println("WRONG ANSWER")
                questions[i].colors[order] = Color.Red
                color = questions[i].colors
            }
        } else {

        }

    }) {
        Text(text = text, fontSize = 30.sp)
    } }

data class Question(
    val sentence: String,
    val options: List<String> = listOf<String>("True", "False"),
    var isChecked: Boolean = false,
    var colors: MutableList<Color>  = mutableStateListOf(Color.White, Color.White),
    var answer: String ) {}
0
Bravetheif On

Ok, I see what you're trying to do here and understand why you did it, but you're heading in the wrong direction here. There isn't a supported way to force recomposition of a composable because the framework was intentionally designed to prevent you from doing so. As a rule of thumb, if you need to force recomposition, you're doing something wrong. That said, let me give you a better structure.

As I'm sure you've gathered, the reason your composable won't update is because nothing is informing its data has changed since it last updated. Composables are lazy, they won't update unless some external factor tells them to. For your use case, your best choice to do so is a MutableLiveData. A LiveData holds data just like a variable, but it also allows parts of your program to "subscribe" to updates of that data. When the data changes, those subscribers are told. Jetpack Compose offers LiveData support in its androidx.compose.runtime:runtime-livedata dependency. From there, you can just call .observeAsState() to have your composable reconstructed every time the LiveData updates. Applied to your code, this could look something like:

@Composable                                //  THIS IS THE FUNCTION WHERE I HAVE THE PROBLEM
fun OptionButton(modifier: Modifier, text: String, i: Int, order: Int) {

    val question by questions.observeAsState(listOf())[i]
    var color = question.colors[order]

    /*if (color == Color.Green) println("color is GREEN")
    if (color == Color.Red) println("color is RED")*/

    Button(modifier = modifier
        .height(80.dp)
        .width(160.dp), colors = ButtonDefaults.buttonColors(
        contentColor = Color.Black, containerColor = question.colors[order]
    ), border = BorderStroke(width = 4.dp, color = Color.Black), onClick = {
        if (question.isChecked == false) {                   // SENTENCE HAS NOT BEEN ANSWERED
            if (question.options[order] == question.answer) {
                question.isChecked = true
                println("RIGHT ANSWER")
                question.colors[order] = Color.Green
                color = question.colors[order]


            } else {                                           // SENTENCE HAS BEEN ANSWERED
                question.isChecked = true
                println("WRONG ANSWER")
                question.colors[order] = Color.Red
                color = question.colors[order]
            }
        } else {

        }

    }) {
        Text(text = text, fontSize = 30.sp)
    }
}

...

val questions = MutableLiveData(listOf<Question>(
    Question("Elephant is the biggest living land animal", answer = "True"),
    Question("Lions fly", answer = "False"),
    Question("Whale is the biggest living animal", answer = "True"),
    Question("Trees walk", answer = "False"),
    Question("Rhinos are mammals", answer = "True"),
    Question("Snakes are vegetarians", answer = "False")
))

To then update questions you would then call questions.value = <new value>. This can be a little convoluted, so I would instead recommend having a structure like this:

// Source of truth
val questions = MutableLiveData<List<Question>>()
private val _questions = listOf<Question>(
    Question("Elephant is the biggest living land animal", answer = "True"),
    Question("Lions fly", answer = "False"),
    Question("Whale is the biggest living animal", answer = "True"),
    Question("Trees walk", answer = "False"),
    Question("Rhinos are mammals", answer = "True"),
    Question("Snakes are vegetarians", answer = "False")
)
        get() = field
        set(value) {
            field = value
            questions.value = value
        }

In this case, _questions is a source of truth for questions. You update it using your existing, simpler code, and it will update itself, then push that change to questions.

Finally, I would recommend looking into Android's Model-View-ViewModel architecture and applying it here. Separating out display and business/data access logic is an important step to making your code more readable and maintainable, especially when using Jetpack Compose.