RecyclerView, with DiffUtil and LiveData

1.8k views Asked by At

I've been stuck on this for a couple of days and could really use some help and understanding on how to setup RecylerView(RV) with DiffUtil, seems whatever I try I can't get the RV to update. I'll run through what I have and hopefully, someone can shed some light on what I'm doing wrong.

App start

  • Create ViewModel,
    • The LiveDataList is then populated in the ViewModel.
  • The RV is instantiated
    • The LayoutManager and the ListAdapter are applied to the RV.
  • ItemTouchHelper is attached to the RecyclerView allowing items to be deleted from the LiveDataList when swiped to the left.
  • The LiveDataList is observed, My understanding is when a change happens to the list, the observer will submit the List to the AdapterList to compare it with the old list if there is a difference between the original submitted listed and the modified list the AdapterList will update the RecyclerView.

Why is the observer not being called to update when an item is added or removed from the list LiveDataList.

class MainActivity : AppCompatActivity() {
    var myAdapter = RVAdapter()
    private lateinit var viewModel: MyViewModel()

    override fun onCreateView(savedInstanceState: Bundle? ) {
        super.onCreate(savedInstanceState)
        // Initialize the ViewModel, that stores the userList. 
        viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
        // Creates list of users
        addUser()

        // Link an RV object to view
        val rv = findViewById<RecyclerView>(R.id.recycler_view)
        // apply RV Settings. 
        rv.apply {
            layoutManager = LinearLayoutManager(baseContext, LinearLayoutManager.VERTICAL, false)
            adapter= myAdapter
        }

        ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT) {
             override fun onMove(...) {
                 // move items isn't used. 
             }

             override fun onSwipe(viewHolder: ViewHolder, direction: Int) {
                 // When I swipe a second item, the app crashes.
                 val editList: MutableList<User>? = viewModel.usersList?.value as MutableList<User>?
                 editList?.removeAt(viewHolder.adapterPosition)
                 viewModel.usersList?.postValue(userList)
             }
        }).attachtoRecyclerView(rv)
     
        // The observer will run after swiping an item but the UI doesn't update. 
        viewModel.usersList?observe(this, Observer {
            it?.let {
                // Should update list but it doesn't
                myAdapter.submitList(it)
            }
    }

    private fun addUser() {
        // Shouldn't there be away to add Items directly to list in ViewModel.
        val newList: MutableList<User> = mutableListOf()
        newList.add(User("Shawn", 1)
        newList.add(User("Shannon", 2)
        newList.add(User("Don", 3)

        viewModel.userList?.postValue(newList)
}

Data class: This is pretty basic and not much happening here.

data class User(val name: String, val accountNumber: Int) {
}

ViewModel: MyViewModel only stores the MutableLiveDat

class MyViewModel : ViewModel() {
    val usersList: MutableLiveData<List<User>>? = MutableLiveData() 
}

RecycleView Adapter

class RVAdapter : ListAdapter<User, RVAdapter.ViewHolder>(MyDiffCallback() {
    
    // This DiffUtil class never gets called not even on startup.
    class MyDiffCallback: DiffUtil.ItemCallback<User>() {
        override fun areItemsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem.name == newItem.name
        }

        override fun areContentsTheSame(oldItem: User, newItem: User): Boolean {
            return oldItem == newItem
        }

    }

    ... Rest of RV is working fine but can be included if it's helpful.  
}

What can be done so when an item is deleted or added from the LiveData List that the observer will pass the modified list to the ListAdapter?

StackTrace included Index outbounds.

2020-10-05 20:14:16.538 19042-19042/com.example.practicerecyclerview2 E/AndroidRuntime: FATAL EXCEPTION: main
    Process: com.example.practicerecyclerview2, PID: 19042
    java.lang.IndexOutOfBoundsException: Index: 2, Size: 2
        at java.util.ArrayList.remove(ArrayList.java:503)
        at com.example.practicerecyclerview2.MainActivity$onCreate$2.onSwiped(MainActivity.kt:100)
        at androidx.recyclerview.widget.ItemTouchHelper$4.run(ItemTouchHelper.java:712)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7356)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:930)
1

There are 1 answers

5
Bram Stoker On BEST ANSWER

The reason your ListAdapter is not updating is that it cannot find a difference in the list you submit, because you already modified it. This way the notifyDataChanged() methods are not called and you get the IndexOutOfBoundsException on a second swipe, because that item does not exist anymore.

The way to solve it is to modify and submit a copy of the list:

override fun onSwipe(viewHolder: ViewHolder, direction: Int) {
    val editList: MutableList<User>? = viewModel.usersList?.value as MutableList<User>?
    val editListCopy = editList?.toMutableList()
    editListCopy?.removeAt(viewHolder.adapterPosition)
    viewModel.users?.postValue(editListCopy)
}