How to force jetpack compose to recompose?
Asked Answered
P

5

32

Say that, I'm building a custom compose layout and populating that list as below

val list = remember { dataList.toMutableStateList()}
MyCustomLayout{
    
   list.forEach { item ->
        key(item){
             listItemCompose( data = item,
                              onChange = { index1,index2 -> Collections.swap(list, index1,index2)})
   }
}

This code is working fine and the screen gets recomposed whenever onChange lambda function is called, but when it comes to any small change in any item's property, it does not recompose, to elaborate that let's change the above lambda functions to do the following

{index1,index2 -> list[index1].propertyName = true} 

Having that lambda changing list item's property won't trigger the screen to recompose. I don't know whether this is a bug in jetpack compose or I'm just following the wrong approach to tackle this issue and I would like to know the right way to do it from Android Developers Team. That's what makes me ask if there is a way to force-recomposing the whole screen.

Psychasthenia answered 5/4, 2021 at 9:40 Comment(5)
if the item is data class. you can use copy. list[index1] = list[index1].copy(propertyName = true) thats make the item in list have new pointer.Exposition
If the propertyName describes something in the UI you need a composable that includes the propertyName as a parameter, then just call it again.Osbert
@Rofie Sagara , I would like to try your suggestion , would you mind clarifying your suggestion with an example.Psychasthenia
@Edward van Raak, I would like to try your suggestion , would you mind clarifying your suggestion with an example.Psychasthenia
change {index1,index2 -> list[index1].propertyName = true} with {index1,index2 -> list[index1] = list[index1].copy(propertyName = true) } and thats make list[index1] address with fresh address cause the copy methodExposition
T
41

You can recreate an entire composition using this:

val text = remember { mutableStateOf("foo") }

key(text.value) {
    YourComposableFun(
        onClick = {
            text.value = "bar"
        }
    ) {

    }
}

In this example the key is text.value, when its value is changed in onClick, everything inside the key function will be recomposed.

Transgress answered 26/7, 2022 at 20:11 Comment(5)
I don't know what this means. Could you expand on this, maybe add some explanation that clarifies what is happening and why? What, for example, is "yourKey"?Kirkcudbright
In this example my key is text.value, when its value is changed in onClick, everything inside the key function will be forced to be recomposed.Transgress
remember can only be called inside composable functionsHolly
Very nice answer, this is kind of similar to remember but for composables themselves. That is, it was very useful for me when even changing parameters some stubborn elements did not want to recompose, key helped very much with that!Louannlouanna
Saved me alot of stressTove
H
19

You can't force a composable function to recompose, this is all handled by the compose framework itself, there are optimizations to determine when something has changed that would invalidate the composable and to trigger a recomposition, of only those elements that are affected by the change.

The problem with your approach is that you are not using immutable classes to represent your state. If your state changes, instead of mutating some deep variable in your state class you should create a new instance of your state class (using Kotin's data class), that way (by virtue of using the equals in the class that gets autogenerated) the composable will be notified of a state change and trigger a recomposition.

Compose works best when you use UDF (Unidirectional Data Flow) and immutable classes to represent the state.

This is no different than, say, using a LiveData<List<Foo>> from the view system and mutating the Foos in the list, the observable for this LiveData would not be notified, you would have to assign a new list to the LiveData object. The same principle applies to compose state.

Hanover answered 5/4, 2021 at 15:54 Comment(2)
By "creating a new instance of state class" , you mean that, I should extract that property from its class and make a state class of it which then makes me create another list of that property's state to end up with two list for each state. This is what I concluded from your answer. If my understanding of your answer is true, how do you think we could deal with case of many states?, is it costly expansive on cpu to have many lists of state?Psychasthenia
dataList is your state. Instead of mutating the list's elements, create a new list containing the updated elements. On modern Android versions, using ART, it is not a concern to allocate many objects like in Dalvik. You also do not need to re-allocate all elements, just the ones that changed, you can reuse those that did not change. The list must be new, but it can contain some of the old elements.Hanover
M
5

If you ever need to force a recompose then create an empty LaunchedEffect with a MutableState<Boolean> value key and toggle the mutable state.

class FooViewHolder {
    private val recomposeToggleState: MutableState<Boolean> = mutableStateOf(false)

    @Composable
    fun View() {
        Log.i("Foo View Holder", "Compose / Recompose")

        Box(modifier = Modifier
            .fillMaxSize()
            .background(Color.Green)
        )

        LaunchedEffect(recomposeToggleState.value) {}
    }

    fun manualRecompose() {
        recomposeToggleState.value = !recomposeToggleState.value
    }
}    

Then in Activity for example

val viewHolder = FooViewHolder()
    
override fun onCreate(savedInstanceState: Bundle?) {
    setContent {
        viewHolder.View()
    }
}

override fun onResume() {
    viewHolder.manualRecompose()
}
Marilee answered 13/10, 2023 at 15:43 Comment(1)
This worked fine up to Kotlin 2.0.10, but with 2.0.20 it fails to force a recompose. The failure may be due to strong skipping now being enabled by default in the Compose Compiler. Instead of including an empty LaunchedEffect, this approach still works if you create an empty function called from inside the Composable with the recomposeToggleState.value as a (dummy) parameter. Of course, this may also be optimized away by a future compiler.Vitrics
A
1

With SnapshotStateList to trigger recomposition you need delete, insert or update existing item with a new item with the property you set as using a data class is best option since it comes out with a copy function

list[index1] = list[index1].copy(propertyName = true)

will trigger recomposition since you set a new item at index1 with property = true

https://mcmap.net/q/1174600/-in-compose-why-modify-the-properties-of-the-list-element-lazycolumn-does-not-refresh

https://mcmap.net/q/851197/-jetpack-compose-lazy-column-all-items-recomposes-when-a-single-item-update

Alten answered 27/4, 2023 at 6:4 Comment(0)
R
0

For enabling compose to track changes to your list's element data type properties, you could use mutableStateOf(initialValue) function to define your property in the data type definition.

as an example:

class Task(
    val id: Int,
    val label: String,
    initialChecked: Boolean = false
) { 
    var checked by mutableStateOf(initialChecked)
}

now, compose will trigger recomposition whenever checked proeprty is mutated.

Rhinitis answered 29/7 at 11:53 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.