Android Jetpack Compose mutableStateListOf not doing Recomposition
Asked Answered
L

5

33

So, I have a mutableStateListOf in viewModel:

var childTravellersList = mutableStateListOf<TravellersDetails>()

TravellersDetails is a data class having a field called error.

this childTravellersList is used in the UI as:

val list = remember{viewModel.childTravellersList}

LazyColumn(state = lazyColumnState) {
    itemsIndexed(list) { index, item ->
        SomeBox(show = if(item.error) true else false)
    }
  }

I have wrote a function in viewModel that updates error of TravellersDetails at given index of childTravellersList as:

fun update(index){
    childTravellersList[index].error = true
}

So, whenever I call this function, the list should get updated.

This updates the list, but UI recomposition is not triggered πŸ˜•. Where am I doing wrong?

Leralerch answered 26/10, 2021 at 5:54 Comment(2)
Great Question. I worked few days on this one as well. Is this mentioned somewhere in the docs ? – Swadeshi
It was there in the docs. But I didn't saw. Thankfully @Philip came as lifesaver! – Leralerch
H
56

mutableStateListOf can only notify about adding/removing/replacing some element in the list. When you change any class inside the list, the mutable state cannot know about it.

Data classes are very good for storing immutable state in unidirectional data flow, because you can always "change" it with copy, while you see the need to pass the new data to view or mutable state. So avoid using var variables with data classes, always declare them as val to prevent such errors.

var childTravellersList = mutableStateListOf<TravellersDetails>()

fun update(index){
    childTravellersList[index] = childTravellersList[index].copy(error = true)
}

An other problem is that you're using val list = remember{viewModel.childTravellersList}: it saves the first list value and prevents updates in future. With ViewModel you can use it directly itemsIndexed(viewModel.childTravellersList)

Horribly answered 26/10, 2021 at 6:51 Comment(4)
This worked for me. I struggled with this for 4 + days :) @Philip Dukhov - out of curiosity - how did you know this ? Is it mentioned somewhere in the docs ? I was trying it like so : childTravellersList[index].count += 1 and expecting recomposition to trigger... – Swadeshi
@KaranAhuja you can find this information in state documentation, it's quite good and full, but I know it's kind of hard to read and understand at once =) – Horribly
Hey this works and is a valid solution but isn't there a way to "tell" compose that the Elements inside my List should notify about my recomposition without the explicit call to .copy kinda likevar var1 by mutableStateOf(2) does but inside a List? – Shcherbakov
@Shcherbakov I think if there was a cleaner way, it would appear in one of the answers here =) – Horribly
P
4

Recomposition will happen only when you change the list itself. You can do it like this.

var childTravellersList by mutableStateOf(emptyList<TravellersDetails>())

fun update(indexToUpdate: Int) {
    childTravellersList = childTravellersList.mapIndexed { index, details ->
        if(indexToUpdate == index) details.copy(error = true)
        else details
    }
}

Also, you need not remember this list in you composable as you have done here val list = remember{viewModel.childTravellersList}. Since the MutableState is inside view model it will always survive all recompositions. Just use the viewModel.childTravellersList inside LazyColumn.

Protuberancy answered 26/10, 2021 at 6:7 Comment(0)
D
3

Use a single triggering mechanism to update your composable. Your list should just be available as a normal variable. You can add/remove/delete items in the list and even update properties of each item. Once you make these updates to the list, you can recompose your composable by generating a random number that is bound to a mutable state observable that is observed in your composable:

Kotlin:

class MyViewModel: ViewModel() {
   var childTravellersList = mutableListOf<TravellersDetails>()
   var onUpdate = mutableStateOf(0)

   private fun updateUI() {
      onUpdate.value = (0..1_000_000).random()
   }

   fun update(index){
       childTravellersList[index].error = true
       updateUI()
   }
}

@Composable
fun MyComposableHandler() {

   // This will detect any changes to data and recompose your composable.
   viewmodel.onUpdate.value
  
   MyComposable(
      travelersList = viewmodel.childTravellersList
   )
}

@Composable
fun MyComposable(
    travelersList: List<TravellersDetails>
) {
   
}

You should avoid creating multiple mutable state variables in your viewmodel for different variables that need updating. There is absolutely no need to do this. Just create a single mutable state variable, as shown above. Whenever you need to recompose your composable, you just updateUI function. Logic in your viewmodel should decide what needs to be updated but leave the actual updating mechanism to a single mutable state observable. This is a clean pattern and will make updating your composables much easier.

However, do keep in mind that your UI is normally going to be made up of many composables in a hierarchy. You don't want to recompose the entire hierarchy when just one element changes. For that reason, a mutable state observable should be used for each composable that needs to be recomposed independently of the others in the heirarchy.

The other benefit of using the solution shown above is that you can easily update objects without the need to create new objects. If you want your composable to recompose when only a certain property of the object changes, you cannot use a mutable state observable because they only detect changes to the object themselves and NOT to the object's properties. This is why you are better off to use the triggering method shown above and simply retrieve the updated object when the composable recomposes.

Devest answered 26/10, 2021 at 7:2 Comment(2)
not working in LazyColumn row:( – Nazar
New to jetpack compose. This was super insightful. Thanks! – Tezel
M
3

State in ViewModel is a really good tutorial that explains on how to manage lists. The following snippet is from the tutorial:

There are two ways to fix this:

  1. Change our data class WellnessTask so that checkedState becomes MutableState instead of Boolean, which causes Compose to track an item change.
  1. Copy the item you're about to mutate, remove the item from your list and re-add the mutated item to the list, which causes Compose to track that list change.
Morez answered 11/4, 2023 at 16:44 Comment(0)
A
0

If you can do without data class,you can take a look at this

Armillas answered 23/6, 2022 at 3:30 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.