imn
imn

Reputation: 890

Android Jetpack Compose mutableStateListOf not doing Recomposition

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?

Upvotes: 37

Views: 47445

Answers (5)

leo
leo

Reputation: 105

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.

Upvotes: 4

Dodd Lanier
Dodd Lanier

Reputation: 21

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

Upvotes: 0

Phil Dukhov
Phil Dukhov

Reputation: 87615

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)

Upvotes: 60

Johann
Johann

Reputation: 29867

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.

Upvotes: 3

Arpit Shukla
Arpit Shukla

Reputation: 10493

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.

Upvotes: 3

Related Questions