Reputation: 540
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
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
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
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:
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