androidandroid-recyclerviewlistadapter

Add Drag and Drop on RecyclerView with DiffUtil


I have a list that gets updated from a Room Database. I receive the updated data from Room as a new list and I then pass it to ListAdapter's submitList to get animations for the changes.

list.observe(viewLifecycleOwner, { updatedList ->
    listAdapter.submitList(updatedList)
})

Now, I want to add a drag and drop functionality for the same RecyclerView. I tried to implement it using ItemTouchHelper. However, the notifyItemMoved() is not working as ListAdapter updates its content through the submitList().

override fun onMove(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    target: RecyclerView.ViewHolder
): Boolean {

    val from = viewHolder.bindingAdapterPosition
    val to = target.bindingAdapterPosition

    val list = itemListAdapter.currentList.toMutableList()
    Collections.swap(list, from, to)

    // does not work for ListAdapter
    // itemListAdapter.notifyItemMoved(from, to)

    itemListAdapter.submitList(list)

    return false
}

The drag and drop now works fine but only when dragged slowly, when the dragging gets fast enough, I get different and inconsistent results.

Screen record

What could be the reason for this? What is the best way that I can achieve a drag and drop functionality for my RecyclerView which uses ListAdapter?


Solution

  • I ended up implementing a new adapter and use it instead of ListAdapter, as mentioned on Martin Marconcini's answer. I added two separate functions. One for receiving updates from Room database (replacement for submitList from ListAdapter) and another for every position change from drag

    MyListAdapter.kt

    class MyListAdapter(list: ArrayList<Item>) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    
        // save instance instead of creating a new one every submit 
        // list to save some allocation time. Thanks to Martin Marconcini
        private val diffCallback = DiffCallback(list, ArrayList())
    
        fun submitList(updatedList: List<Item>) {
            diffCallback.newList = updatedList
            val diffResult = DiffUtil.calculateDiff(diffCallback)
    
            list.clear()
            list.addAll(updatedList)
            diffResult.dispatchUpdatesTo(this)
        }
    
        fun itemMoved(from: Int, to: Int) {
            Collections.swap(list, from, to)
            notifyItemMoved(from, to)
        }
    
    }
    

    DiffCallback.kt

    class DiffCallback(
        val oldList: List<Item>,
        var newList: List<Item>
    ) : DiffUtil.Callback() {
    
        override fun getOldListSize(): Int {
            return oldList.size
        }
    
        override fun getNewListSize(): Int {
            return newList.size
        }
    
        override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return oldItem.id == newItem.id
        }
    
        override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
            val oldItem = oldList[oldItemPosition]
            val newItem = newList[newItemPosition]
            return compareContents(oldItem, newItem)
        }
    }
    

    Call itemMoved every position change:

    override fun onMove(
        recyclerView: RecyclerView,
        viewHolder: RecyclerView.ViewHolder,
        target: RecyclerView.ViewHolder
    ): Boolean {
        val from = viewHolder.bindingAdapterPosition
        val to = target.bindingAdapterPosition
        itemListAdapter.itemMoved(from, to)
    
        // Update database as well if needed
    
        return true
    }
    

    When receiving updates from Room database:

    You may also want to check if currently dragging using onSelectedChanged if you are also updating your database every position change to prevent unnecessary calls to submitList

    list.observe(viewLifecycleOwner, { updatedList ->
        listAdapter.submitList(updatedList)
    })