0

In my app I display a list of outfits in a 2 column GridLayout RecyclerView, and allow users to swipe an outfit to the side. Upon swiping, I update the viewIndex of the outfit in the database (an integer which it uses for sorting the result of the "get all outfits" query), which causes the query my LiveData (generated by Room) is watching to change and put that item at the end of the returned list. This in turn calls a setList method in my RecyclerViewAdapter which uses DiffUtil to update the list.

Everything works as expected in most cases. An item is swiped to the side, disappears, and if you scroll to the bottom of the RecyclerView you can find it again at the end.

However, when the position in the RecyclerView where this swiped item should appear (i.e. the bottom) is currently visible to the user, the item does not appear. If additional items are swiped while the end is still visible, they won't appear either.

Upon scrolling up and then back down, the items will now be in their proper places - it's fixed. I do not know why they are not rendered intially though - is this something to do with DiffUtil perhaps? It could also have to do with my solution to this bug, where I save and restore the state of the RecyclerView either side of the setList call to prevent it scrolling to the new location when the first item of the list is moved (see BrowseFragment below). I admit, I do not know exactly what that code does, I only know it fixed that problem. I tried commenting out those lines but it didn't affect the disappearing views.

How can I ensure the swiped items display immediately without requiring a scroll up? Below is a gif demonstrating the feature in use and then showing the problem (sorry for low quality, had to fit under 2MB).

gif of problem

Code in BrowseFragment.java where RecyclerView and Adapter are initialised:

RecyclerView outfitRecyclerView = binding.recyclerviewOutfits;
        outfitsAdapter = new OutfitsAdapter(this, getViewLifecycleOwner(), this);
        RecyclerView.LayoutManager layoutManager = new GridLayoutManager(requireActivity(), GRID_ROW_SIZE);
        outfitRecyclerView.setLayoutManager(layoutManager);
        outfitRecyclerView.setAdapter(outfitsAdapter);

        //observe all outfits
        outfitViewModel.getAllOutfits().observe(getViewLifecycleOwner(), (list) -> {
            //save the state to prevent the bug where moving the first item of the list scrolls you to its new position
            Parcelable recyclerViewState = outfitRecyclerView.getLayoutManager().onSaveInstanceState();
            //set the list to the adapter
            outfitsAdapter.setList(list);
            outfitRecyclerView.getLayoutManager().onRestoreInstanceState(recyclerViewState);
        });

Room dao query used to generate the LiveData observed in outfitsViewModel.getAllOutfits()

@Query("SELECT * FROM outfits ORDER BY view_queue_index ASC")
LiveData<List<Outfit>> getAll();

setList method in OutfitsAdapter.java, where outfits is a private member variable containing the current list of outfits.

...
public void setList(List<Outfit> newList){
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new OutfitDiff(newList, outfits));
    diffResult.dispatchUpdatesTo(this);
    outfits = newList;
    outfitsFull = new ArrayList<>(newList);
}

private class OutfitDiff extends DiffUtil.Callback {
    List<Outfit> newList;
    List<Outfit> oldList;

    public OutfitDiff(List<Outfit> newList, List<Outfit> oldList) {
        this.newList = newList;
        this.oldList = oldList;
    }

    @Override
    public int getOldListSize() {
        if(oldList == null){
            return 0;
        }
        return oldList.size();
    }

    @Override
    public int getNewListSize() {
        if(newList == null){
            return 0;
        }
        return newList.size();
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return oldList.get(oldItemPosition).getId() == newList.get(newItemPosition).getId();
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        return oldList.get(oldItemPosition).equals(newList.get(newItemPosition));
    }
}
Trex
  • 108
  • 9
  • Share your DiffUtil implementation. Don't reference a new list object to your outfits each time the list has changed. Instead use always the same reference in this way: `outfits.clear();` `outfits.addAll(newList);` – Kozmotronik Jul 28 '21 at 07:14
  • @Kozmotronik I have shared the DiffUtil implementation as requested. Also, what is your reasoning behind clearing/refilling the list each time? Surely this would be slower, and the old list object would simply be cleaned up by garbage collector anyway. – Trex Aug 05 '21 at 03:29
  • generally for safety and data integrity reasons. See either [this answer](https://stackoverflow.com/a/37549473/12749998) and [this answer](https://stackoverflow.com/a/37549493/12749998) for brief description. – Kozmotronik Aug 05 '21 at 06:22
  • @Kozmotronik does the added code of DiffUtil implementation help you understand the actual problem of this question though? – Trex Aug 05 '21 at 23:21
  • Umm not exactly in fact. But your issue seems to be related to the an organisation lack among your adapter, ItemTouchHelper and DiffUtil. By the way you use ItemtouchHelper for swipe operations don't you? If you do, probably your diff util get no updates of your swipe operation. How do you inform the adapter of the swipe to delete operation? Do you invoke notifyItemRemoved() properly after swipe? – Kozmotronik Aug 06 '21 at 06:05
  • The item is not removed, the underlying `Outfit` object simply has its `viewQueueIndex` property updated to be the highest number of all the Outfits and this change is persisted to my Room database. This triggers a change in the `LiveData` returned by my `getAllOutfits` query, which triggers `setList`, which in turn will trigger `DiffUtil`. – Trex Aug 09 '21 at 01:29

0 Answers0