Zebulon Li
Zebulon Li

Reputation: 540

how can drag item in RecyclerView work with paging library together?

My app has a RecyclerView which support drag items to change their order. My app use ViewModel, Lifecycle, Room before adding paging library. And code to handle drag is easy.

override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        val oPosition = viewHolder.adapterPosition
        val tPosition = target.adapterPosition
        Collections.swap(adapter?.data ,oPosition,tPosition)
        adapter?.notifyItemMoved(oPosition,tPosition)
        //save to db
        return true
    }

However, after I use paging library,

override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        val oPosition = viewHolder.adapterPosition
        val tPosition = target.adapterPosition
        Collections.swap(adapter.currentList,oPosition,tPosition)
        adapter.notifyItemMoved(oPosition,tPosition)
        return true
    }

my app crashed because PagedListAdapter.currentList do not support set.

    java.lang.UnsupportedOperationException
    at java.util.AbstractList.set(AbstractList.java:132)
    at java.util.Collections.swap(Collections.java:539)
    at gmail.zebulon988.tasklist.ui.TaskListFragment$MyItemTouchCallback.onMove(TaskListFragment.kt:119).

Then I change the code

override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
        val oPosition = viewHolder.adapterPosition
        val tPosition = target.adapterPosition
        Log.d("TAG","onMove:o=$oPosition,t=$tPosition")
        val oTask = (viewHolder as VH).task
        val tTask = (target as VH).task

        if(oTask != null && tTask != null){
            val tmp = oTask.order
            oTask.order = tTask.order
            tTask.order = tmp
            tasklistViewModel.insertTask(oTask,tTask)

        }
        return true
    }

This code change the task's order in db directly and the library update the display order by the db change. However, the animation is ugly.

Is there a way to use onMove and paging library together genteelly?

Upvotes: 1

Views: 1600

Answers (3)

miredirex
miredirex

Reputation: 402

I had a slightly different problem and @dmapr's answer has finally led me to the solution after hours of debugging.

For me the issue was that the item I just moved suddenly jumped back to its previous position after the db was updated and the call to submitData was made. Basically the drag and drop action is sort of canceled, however the order with all the relevant data is correct in the database, and if I was to call notifyDataSetChanged() I'd see the real list where all items are where they should be. Here's what has worked for me:

class SomePagingAdapter(
    private val onItemMoveUpdate: (fromPos: Int, toPos: Int) -> Unit,
) : PagingDataAdapter<Model, SomePagingAdapter.ViewHolder>(diffUtil), ItemMoveCallback {

    companion object {
        private val diffUtil = /* ... */
    }

    private var swapInfo: SwapInfo? = null

    // viewHolder methods, etc.

    // Called in touch helper's onMove
    override fun onItemMove(fromPos: Int, toPos: Int) {
        notifyItemMoved(fromPos, toPos)
    }

    // Called in touch helper's clearView() to save the result of this drag and drop
    override fun onItemFinishedMove(fromPos: Int, toPos: Int) {
        swapInfo = SwapInfo(fromPos, toPos)
        onItemMoveUpdate(fromPos, toPos)
    }

    fun adjustRecentSwapPositions() {
        // "Undo" the notifyItemMoved we did before that messed up positions
        swapInfo?.let { swap ->
            notifyItemMoved(swap.toPos, swap.fromPos)
        }
        swapInfo = null
    }
}

interface ItemMoveCallback {
    fun onItemMove(fromPos: Int, toPos: Int)
    fun onItemFinishedMove(fromPos: Int, toPos: Int)
}

data class SwapInfo(val fromPos: Int, val toPos: int)

It's important that submitData is suspended and adjustRecentSwapPositions is called immediately after. Watch out for that if you use RxJava.

scope.launch {
    flow.collectLatest { pagingData ->
        adapter.submitData(pagingData)
        adapter.adjustRecentSwapPositions()
    }
}

It works great and recycler's animations are fine.

Upvotes: 1

TonnyTao
TonnyTao

Reputation: 532

You need to heck for moving items in PagedList.

Recyclerview's adapter needs to do two things perfectly if you want to drag items up and down for moving them. First is to swap two items in datalist, second is to notify cells re-render.

re-render is easy, you can use notifyItemMoved to update layout when moving, but PagedList is immutable, you cannot modify it.

And there is an animation bug when the cell ui has already changed but the datasource did not. You cannot override the render logic in inner of recyclerview, but you can heck the result of PagedStorageDiffHelper.computeDiff to fix the animation bug.

At last, dont forget to retrieve the most updated data after the drag and drop.

//ItemTouchHelperAdapter

override fun onItemStartMove() {
    //the most the most updated data; mimic pagedlist, but can be modified;
    tempList = adapter.currentList?.toMutableList()
    toUpdate = mutableListOf()
}

override fun onItemMove(fromPosition: Int, toPosition: Int): Boolean {
    val itemFrom = tempList?.get(fromPosition) ?: return false
    val itemTo = tempList?.get(toPosition) ?: return false

    //change order property for data itself
    val order = itemTo.order
    itemTo.order = itemFrom.order
    itemFrom.order = order

    //save them for later update db in batch
    toUpdate?.removeAll { it.id == itemFrom.id || it.id == itemTo.id }
    toUpdate?.add(itemFrom)
    toUpdate?.add(itemTo)

    //mimic mutable pagedlist, for get next time get correct items for continuing drag
    Collections.swap(tempList!!, fromPosition, toPosition)

    //update ui
    adapter.notifyItemMoved(fromPosition, toPosition)

    return true
}

override fun onItemEndMove() {
    tempList = null

    if (!toUpdate.isNullOrEmpty()) {
        mViewModel.viewModelScope.launch(Dispatchers.IO) {

            //heck, fix animation bug because pagedList did not really change.
            NoteListAdapter.disableAnimation = true

            mViewModel.updateInDB(toUpdate!!)

            toUpdate = null
        }
    }
}
//Fragment

mViewModel.data.observe(this.viewLifecycleOwner, Observer {
    adapter.submitList(it)

    //delay for fix PagedStorageDiffHelper.computeDiff running in background thread
    if (NoteListAdapter.disableAnimation) {
        mViewModel.viewModelScope.launch {
            delay(500)

            adapter.notifyDataSetChanged() //update viewholder's binding data
            NoteListAdapter.disableAnimation = false
        }
    }
})
//PagedListAdapter

companion object {
    //heck for drag and drop to move items in PagedList
    var disableAnimation = false

    private val DiffCallback = object : DiffUtil.ItemCallback<Note>() {

        override fun areItemsTheSame(old: Note, aNew: Note): Boolean {
            return disableAnimation || old.id == aNew.id
        }

        override fun areContentsTheSame(old: Note, aNew: Note): Boolean {
            return disableAnimation || old == aNew
        }
    }
}

Upvotes: 1

dmapr
dmapr

Reputation: 359

When you use a PagedList with Room you often tie it up so that the updates to the underlying data are reflected automatically via LiveData or Rx, and such an update happening in a background can always mess up your drag and drop. So IMHO you can't make it 100% bulletproof for all situations. Having said that, you can create (I almost said "hack together") a shim that will do what you want. This involves several pieces:

  1. You need to hold the indexes of the items being swapped in your adapter
  2. You need to override getItem() in the adapter and make it "swap" the items for you instead of swapping them using Collections.swap
  3. You need to delay the actual item updating via Room until the item is dropped, at which point you also clear your "swapping in progress" state. Something along these lines:

    fun swapItems(fromPosition: Int, toPosition: Int) {
        swapInfo = SwapInfo(fromPosition, toPosition)
        notifyItemMoved(fromPosition, toPosition)
    }
    
    override fun getItem(position: Int): T? {
        return swapInfo?.let {
            when (position) {
                it.fromPosition -> super.getItem(it.toPosition)
                it.toPosition -> super.getItem(it.fromPosition)
                else -> super.getItem(position)
            }
        } ?: super.getItem(position)
    }
    
    fun clearSwapInfo() {
        swapInfo = null
    }
    

This way you will get a smooth dragging experience as long as there are no background updates for your list and you stay within already loaded list of items. It gets much more complicated if you need to be able to drag through a "refill".

Upvotes: 5

Related Questions