Android Jetpack compose - Observe a live data not trigger after second channge

Viewed 4884

I'm using viewModel to pass data to my compose view. The thing is I wanna handle expand and collapse view by filed inside my models in viewModel. So If some UI click on expand method I will call viewModel and doExpand method like this:

 private val _acquiredCoupons = MutableLiveData<List<AcquiredCoupon>>(listOf())
 val acquiredCoupons: LiveData<List<AcquiredCoupon>> = _acquiredCoupons

    fun doExpand(coupon : AcquiredCoupon){
        
        val index = _acquiredCoupons.value?.indexOf(coupon) ?: -1
         val newItems = _acquiredCoupons.value?.toMutableList().also {
            it?.get(index)?.isExpanded = true
        }
        _acquiredCoupons.value = newItems
        Timber.i("Mahdi $index")
    }

But the thing is after update models in compose UI will not trigger and re compose is not happening! I'm observing the live data like this:

val items = viewModel.acquiredCoupons.observeAsState(initial = emptyList())
3 Answers

If anyone is still having this issue observing a list, I found a good solution was to switch from List to SnapshotStateList in the ViewModel/LiveData.

After converting from this:

class MainViewModel : ViewModel() {
    val itemLiveData: LiveData<List<String>>
        get() = items

    private val items = MutableLiveData<List<String>>()
    private val itemImpl = mutableListOf<String>()

    fun addItem(name: String){
        itemImpl.add(name)
        items.postValue(itemImpl)
    }
}

To this form:

class MainViewModel : ViewModel() {
    val itemLiveData: LiveData<SnapshotStateList<String>>
        get() = items

    private val items = MutableLiveData<SnapshotStateList<String>>()
    private val itemImpl = mutableStateListOf<String>()

    fun addItem(name: String){
        itemImpl.add(name)
        items.postValue(itemImpl)
    }
}

My composable worked as expected with when items were added to the list

@Composable
fun ItemList(model: MainViewModel = viewModel()) {
    Column {
        Button( 
            onClick = { model.addItem("New item") },
            content = { Text(text="Add item") } 
        )
                
        val items by model.itemLiveData.observeAsState()

        if( items.isNullOrEmpty() ) {
            Text(text = "No items")
        }
        else {
            items?.forEach { item ->
                Text(text=item)
            }
        }
    }
}

In your observing code, you need to use observeAsState, so your view will be recomposed with any LiveData updates that changes the value.

val items by viewModel.acquiredCoupons.observeAsState()

The issue is likely that you're passing the raw value to the Composable (please include a sample snippet to your question when you get a chance). If the value is considered equal to the previous value, a recomposition will not happen.

One way you can get around this is by passing the State object itself to the target Composable.

So for example if you were setup like so:

@Composable
fun CouponBar(
    coupons: List<Coupon>
) { ... }

then try

@Composable
fun CouponBar(
    coupons: State<List<Coupon>>
) { ... }
Related