20

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.

Odai A. Ali
  • 845
  • 3
  • 8
  • 23
  • 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. – Rofie Sagara Apr 05 '21 at 10:16
  • If the `propertyName` describes something in the UI you need a composable that includes the `propertyName` as a parameter, then just call it again. – Edward van Raak Apr 05 '21 at 10:27
  • @Rofie Sagara , I would like to try your suggestion , would you mind clarifying your suggestion with an example. – Odai A. Ali Apr 05 '21 at 10:55
  • @Edward van Raak, I would like to try your suggestion , would you mind clarifying your suggestion with an example. – Odai A. Ali Apr 05 '21 at 10:55
  • 1
    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 method – Rofie Sagara Apr 05 '21 at 12:43

4 Answers4

16

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.

Francesc
  • 25,014
  • 10
  • 66
  • 84
  • 1
    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? – Odai A. Ali Apr 05 '21 at 17:38
  • 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. – Francesc Apr 05 '21 at 17:45
15

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.

evcostt
  • 179
  • 1
  • 5
  • 1
    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"? – meesern Nov 29 '22 at 12:38
  • 1
    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. – evcostt Nov 30 '22 at 13:23
  • `remember` can only be called inside composable functions – IgorGanapolsky Jan 16 '23 at 03:21
  • 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! – Skydev May 21 '23 at 13:33
2

call this

currentComposer.composition.recompose()
EunhaEonnie
  • 223
  • 2
  • 4
  • Any examples or situations showing when and where you will call this? – z.g.y Nov 22 '22 at 11:13
  • I get this error when using your suggestion: `androidx.compose.runtime.ComposeRuntimeError: Compose Runtime internal error. Unexpected or incorrect use of the Compose internal runtime API (pending composition has not been applied). Please report to Google or use https://goo.gle/compose-feedback at androidx.compose.runtime.ComposerKt.composeRuntimeError(Composer.kt:4383)` – StefanTo Feb 27 '23 at 15:09
  • 1
    You get this error because, as the error says, you're misusing the Compose API. You should never use `currentComposer` directly – Luke Needham Jun 02 '23 at 12:55
0

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://stackoverflow.com/a/74506067/5457853

https://stackoverflow.com/a/74700668/5457853

Thracian
  • 43,021
  • 16
  • 133
  • 222