How do I copy the existing ListAdapter list so that it can be used to move items and then update the UI by comparing the new, copied list to the original list?
Perhaps the Android devs only thought of CRUD operations when they designed the source code for ListAdapter to be used for RecyclerView lists. If so, they neglected to account for other vital recyclerView operations like filtering, sorting and moving list items. The only thing that is changing for those operations is the sort order (sorting and moving items) or removing some items from the original list (for filtering) but that would now require a CustomAdapter? Also, the requirement to copy a list into memory, for a very large RecyclerView list so the ListAdapter "sees" a new list, seems to defeat the original purpose of the ListAdapter to provide efficiency gains on a background thread.
I embraced converting my existing project to ListAdapter with the thought that updates on a background thread would be very valuable for stability and UI efficiency. I have CRUD ops working fine but now the daunting task is to figure out how to do those other filtering, sorting and moving list items.
I tried to move items in the RecyclerView list but drag-and-drop does not work. Below are the different methods I've tried so far in various combinations. In the meantime, I read every other ListAdapter question on stackoverflow...and unfortunately I know zero Kotlin so I am looking for a Java solution. What am I missing here?
MainActivity approach #1:
ItemTouchHelper.SimpleCallback itemTouchHelperCallback1 = new ItemTouchHelper.SimpleCallback(
(ItemTouchHelper.UP | ItemTouchHelper.DOWN),0) {
int dragFrom = -1;
int dragTo = -1;
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {
int fromPos = viewHolder.getBindingAdapterPosition();
int toPos = target.getBindingAdapterPosition();
if (dragFrom == -1) {
dragFrom = fromPos;
}
dragTo = toPos;
cardsAdapter.moveItem(fromPos, toPos);
return true;
}
};
new ItemTouchHelper(itemTouchHelperCallback1).attachToRecyclerView(recyclerView);
MainActivity approach #2, for the onMove() method:
List newList = adapter.getCurrentList();
Quickcard quickcardItem = newList.get(fromPos);
newList.remove(fromPos);
newList.add(toPos, quickcardItem);
cardsAdapter.moveItem(fromPos, toPos);
cardsAdapter.notifyItemMoved(fromPos, toPos);
cardsAdapter.submitList(newList);
return true;
ListAdapter approach #1:
public class CardsAdapter extends ListAdapter<Quickcard, CardsAdapter.ItemHolder> {
private final LayoutInflater layoutInflater;
protected CardsAdapter(@NonNull LayoutInflater layoutInflater, DiffUtil.ItemCallback<Quickcard> diffCallback) {
super(diffCallback);
this.layoutInflater = layoutInflater;
}
... // ViewHolder code
public void moveItem(int fromOldPos, int toNewPos) {
// Copy the ListAdapter's existing list
ArrayList<Quickcard> currList = new ArrayList<>(getCurrentList());
if (fromOldPos == toNewPos) {
return;
}
Collections.swap(currList, toNewPos, fromOldPos);
notifyItemMoved(fromOldPos, toNewPos);
submitList(currList);
}
}
ListAdapter approach #2, for the moveItem() method:
List<Quickcard> currList = getCurrentList();
List<Quickcard> copiedList = new ArrayList<>(currList);
if (fromOldPos == toNewPos) {
return;
}
if (fromOldPos < toNewPos) { // the q. is being moved down the q.list
for (int i = fromOldPos; i < toNewPos; i++) {
Collections.swap(copiedList, i,i + 1);
}
} else { // the q. is being moved up the q.list
for (int i = fromOldPos; i > toNewPos; i--) {
Collections.swap(copiedList, i,i - 1);
}
}
notifyItemMoved(fromOldPos, toNewPos);
submitList(copiedList);
// after adding a copy constructor to the Model, I also tried this
// (see @Matt's answer stackoverflow #48575477),
ListAdapter approach #3, for the moveItem() method:
List<CustomObject> copyList = new ArrayList<>();
for(CustomObject obj : parentList) {
copyList.add(new CustomObject(obj));
}
I also tried this approach, in the ViewModel:
// mTodos is the MutableLiveData<List> in the ViewModel
List<Todo> todos = mTodos.getValue();
ArrayList<Todo> clonedTodos = new ArrayList<Todo>(todos.size());
for(int i = 0; i < todos.size(); i++){
clonedTodos.add(new Todo(todos.get(i)));
}
mTodos.postValue(clonedTodos);
Unfortunaly the
ListAdapterby itself has trouble with drag/swipe operations, but most of the nice features of thesubmitListmethod can be duplicated usingDiffUtilmethods in a regular adapter. You can handle filtering by filtering the list elsewhere (e.g. in the ViewModel) and the posting it withsubmitList. By doing this, you have access to the displayed list and can modify it easily for drag/swipe events, while still getting some of the benefits of theListAdapterwhen posting new lists or re-ordered lists.Here's an example of how to do that and get a working drag/swipe behavior with "ListAdapter-like" behavior for
submitListthat usesDiffUtiltools. You do lose the background thread for the diff callback. That could be added back in tosubmitListwith a bit more difficulty (or you could use Kotlin and coroutines to add it more easily).The adapter
Since the adapter has access to the displayed list, it can move items around in it, or remove them, when items are moved/swiped away. When a new list is posted with
submitList, it uses the DiffUtil methods to compare the new list to the currently displayed list, updates the displayed list, and dispatches changes to the recycler view to be animated.If you need to propagate order changes/swiping back to a ViewModel or other layer, pass an interface to the adapter and call it in
onItemMoveandonItemDismissas appropriate to let the backend know that item position changed or items were removed. Such changes should not cause the backend to submit a new list tosubmitListsince the displayed list is already updated.Activity
The item touch interface
(not strictly necessary, but nice to decouple the adapter from the item touch helper)
The item touch callback
Simple callback that just calls
onItemMoveandonItemDismisson the adapter.The data class