0
votes

I want to implement swipe to delete functionality in a RecyclerView which is populated with data from an API. There is a ViewModel class and data is fetched from API to a allOrders: MutableLiveData<MutableList<ModelClass>> in ViewModel. The observer in Fragment updates the RecyclerView Adapter on LiveData change:

viewModel.allOrders.observe(viewLifecycleOwner, Observer {
    adapter.submitList(it)
})

I want to delete the swiped item in RecyclerView from allOrders: MutableLiveData<MutableList<ModelClass>>, so this is the function to do that in ViewModel class:

fun deleteItem(index: Int) {
    _allOrders.value?.removeAt(index)
}

But removing an item from MutableLiveData<MutableList> doesn't even notify the observer.

What is the best practice for implementing such a functionality using ViewModel and LiveData in Kotlin?

UPDATE

I change deletItem function to this:

fun deleteItem(index: Int) {
    _allOrders.value = _allOrders.value?.also { list ->
    list.removeAt(index)
}

Now the observer is being notified, but adapter.submitList(it) doesn't change RecyclerView to new list. Scrolling the RecyclerView throw IndexOutOfBoundsException.

The adapter class looks like this one in codeslab example

3

3 Answers

1
votes

You can have a 'reference list' in your ViewModel and operate on it. When the item is removed, then update the list and update the LiveData with this list:

class YourViewModel: ViewModel() {

    private var ordersList = MutableList<ModelClass>()
    val _allOrders = MutableLiveData<MutableList<ModelClass>>()
    
    fun deleteItem(index: Int) {
        ordersList.removeAt(index)
        _allOrders.value = ordersList
    }
}

You can also use Kotlin scope function called also without adding the globally scoped list if you don't need it and operate only on values emitted by LiveData:

fun deleteItem(index: Int) {
    _allOrders.value = _allOrders.value?.also { list -> 
       list.removeAt(index)
   }
}
1
votes

The way you've implemented deleteItem(), circumvents the LiveData class. You're getting a reference to the List that is stored as it's value, and directly manipulating it, LiveData is unaware that anything has happened to the data.

Since Kotlin is a pass-by-reference language, the value that LiveData is holding is just a reference to the memory where the array is held, which means that if you change the contents of the array, the LiveData.value has not actually changed in any way.

You'll have to set a new value on the LiveData in order to update it and notify observers, in the case of a List, you're either gonna have to create a whole new List and set it as the value, or do this:

fun deleteItem(index: Int) {
    _allOrders.value?.removeAt(index)
    _allOrders.value = _allOrders.value
}
1
votes

I had the same problem, submitList() was not updating the view. Found the solution here

Basically, the adapter class stores a reference to current list (call it curList). When submitList is called with a newList, it compares curList and newList, if they reference the same list, it silently ignores updating the view.

In the above solutions we are removing element in the list, so the reference is not changing. So when we call submitList with this new list, the function sees it as the same list, so it ignores updating view.

One solution is:

adapter.submitList(null);
adapter.submitList(it);

But this will call OnBindView on all list elements again, ignoring the DiffUtilCallBack.

Another is to create a shallow copy:

adapter.submitList(mutableListOf(it))

Make sure you call submitList this way at all places where the list changes.

or make allOrders: MutableLiveData<List<ModelClass>> and:

fun deleteItem(index: Int) {
    // minus() creates a new list automatically
    _allOrders.value = _allOrders.value?.minus(_allOrders.value?.get(index))
}

In this way, you can just call submitList(it).