I have a RecyclerView which allows swipe-to-delete functionality. After deliting, a Snackbar shows to confirm deletion with an action that allows users to "undo" the delete.
Everything works fine until I delete the item at position 0 then hit undo
. The item will be reinserted back into the list but users will need to scroll up to bring it back into view.
What I have tried
Setting
recyclerView.isNestedScrollingEnabled"
on the RecyclerViewUsing
layoutCoordinator.scrollTo(0, 0)
on the Coordinator LayoutUsing
recyclerView.smoothScrollToPosition(0)
on the RecyclerViewUsing
recyclerView.scrollToPosition(0)
on the RecyclerViewUsing
recyclerView.scrollTo(0, 0)
on the RecyclerViewUsing
itemAdapter.notifiyItemInserted(0)
on the AdapterUsing
itemAdapter.notifiyItemchanged(0)
on the AdapterUsing
itemAdapter.notifyDataSetChanged()
on the AdapterUsing
layoutManager.scrollToPositionWithOffset(0, 0)
on the LayoutManagerCreating a custom
LayoutManager
and overridingsmoothScrollToPosotion()
with my own implementation.
None of the above have offered a solution.
Below are the workings.
ItemsFragment
Inside onCreateView
- here is setting up the itemAdapter
and recyclerView
:
val itemAdapter = object : ItemRecyclerViewAdapter() {
override fun onItemClicked(item: Item) {
// todo
}
}
val layoutManager = LinearLayoutManager(context)
recyclerView.layoutManager = layoutManager
recyclerView.adapter = itemAdapter
Here is my swipeToDeleteCallback
with Snackbar
action to "undo" the delete:
val swipeToDeleteCallback = object : SwipeToDeleteCallback() {
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
val position = viewHolder.adapterPosition
val item = itemAdapter.currentList[position]
viewModel.deleteItem(item)
val snackBar = Snackbar.make(
recyclerView,
getString(R.string.snackbar_msg_deleted_card),
Snackbar.LENGTH_LONG
)
snackBar.setAction(R.string.snack_bar_undo) {
viewModel.restoreItem(item)
recyclerView.smoothScrollToPosition(position)
}
snackBar.show()
}
}
val itemTouchHelper = ItemTouchHelper(swipeToDeleteCallback)
itemTouchHelper.attachToRecyclerView(recyclerView)
Setting the items which are LiveData observed from the ViewModel. Changes are immediately passed to the adapter:
val items = viewModel.items.await()
items.observe(viewLifecycleOwner, Observer {
it?.let {
itemAdapter.setItems(it)
}
})
ItemsAdapter
Submitting the list with DiffUtilCallback:
fun setItems(list: List<Item>?) {
adapterScope.launch {
val itemsList = when(list) {
null -> emptyList()
else -> list.sortedByDescending {
it.itemId
}
}
withContext(Dispatchers.Main) {
submitList(itemsList)
}
}
}
Temporary fix
So far the only thing that has worked is this hacky solution inside my Snackbar
action:
snackBar.setAction(R.string.snack_bar_undo) {
viewModel.restoreItem(item)
if (position == 0) {
itemsAdapter.notifyItemInserted(0)
recyclerView.smoothScrollToPosition(0)
}
}
Here I'm checking if the item position
is 0
. If so then telling the adapter that there's a new item
inserted at position 0
- then initiating scrolling. Otherwise, don't do anything because items at any position
other than 0
will insert and animate fine beceause of the DiffUtilCallback
.
This temporary fix works but the scrolling is "snappy" and produces an error in the logs:
RecyclerView: Passed over target position while smooth scrolling
Also, this solution does not work 100% of the time. It's more like 60% of the time.
Does anybody know of a better solution/something I am missing and a way to resolve the error above?