Android how to sort RecyclerView List when using AsyncListDiffer?

1k Views Asked by At

I have a RecyclerView that shows a list of CardViews. I recently switched the project from using RecyclerView Adapter to using an AsyncListDiffer Adapter to take advantage of adapter updates on a background thread. I have converted over all previous CRUD and filter methods for the list but cannot get the sort method working.

I have different types or categories of CardViews and I would like to sort by the types/categories. I clone the existing list mCards so the "behind the scenes" DiffUtil will see it as a different list, as compared to the existing list that I wanted to sort. And then I use AsynListDiffer's submitList().

The list is not sorting. What am I missing here?

MainActivity:

private static List<Card> mCards = null;

...
mCardViewModel = new ViewModelProvider(this).get(CardViewModel.class);
mCardViewModel.getAllCards().observe(this,(cards -> {

    mCards = cards;
    cardsAdapter.submitList(mCards);
})); 
mRecyclerView.setAdapter(cardsAdapter);

A click on a "Sort" TextView runs the following code:

ArrayList<Card> sortItems = new ArrayList<>();
for (Card card : mCards) {
    sortItems.add(card.clone());
}
Collections.sort(sortItems, new Comparator<Card>() {
    @Override
    public int compare(Card cardFirst, Card cardSecond) {
        return cardFirst.getType().compareTo(cardSecond.getType());
    }
});
cardsAdapter.submitList(sortItems);
// mRecyclerView.setAdapter(cardsAdapter);  // Adding this did not help

AsyncListDifferAdapter:

public AsyncListDifferAdapter(Context context) {

    this.mListItems = new AsyncListDiffer<>(this, DIFF_CALLBACK);
    this.mContext = context;
    this.mInflater = LayoutInflater.from(mContext);
}

public void submitList(List<Quickcard> list) {

    if (list != null) {
        mListItems.submitList(list);
    }
}

public static final DiffUtil.ItemCallback<Card> DIFF_CALLBACK
        = new DiffUtil.ItemCallback<Card>() {

    @Override
    public boolean areItemsTheSame(@NonNull Card oldItem, @NonNull Card newItem) {

        // User properties may have changed if reloaded from the DB, but ID is fixed
        return oldItem.getId() == newItem.getId();
    }
    @Override
    public boolean areContentsTheSame(@NonNull Card oldItem, @NonNull Card newItem) {
        return oldItem.equals(newItem);
    }

    @Nullable
    @Override
    public Object getChangePayload(@NonNull Card oldItem, @NonNull Card newItem) {
        return super.getChangePayload(oldItem, newItem);
    }
};

Model:

@Entity(tableName = "cards")
public class Card implements Parcelable, Cloneable {
// Parcelable code not shown for brevity
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "cardId")
public int id;
@ColumnInfo(name = "cardType")
private String type;

@Ignore
public Card(int id, String type) {
    this.id = id;
    this.type = type;
}

public int getId() {
    return this.id;
}
public String getType() {
    return this.type;
}

@Override
public boolean equals(Object obj) {

    if (obj == this)
        return true;

    else if (obj instanceof Card) {

        Card card = (Card) obj;

        return id == card.getId() &&
            type.equals(card.getType());
    } else {
        return false;
    }
}  

@NonNull
@Override
public Card clone() {
    Card clone;
    try {
        clone = (Card) super.clone();
    } catch (CloneNotSupportedException e) {
        throw new RuntimeException(e);
    }
    return clone;
}  
3

There are 3 best solutions below

4
On BEST ANSWER

Instead of using notifyDataSetChanged() we can use notifyItemMoved(). That solution gives us a nice animation of sorting. I put the sort order within the adapter. We need a displayOrderList that will contain the currently displayed elements because mDiffer.getCurrentList() doesn't change the order of elements after notifyItemMoved(). We first moved the element that is first sorted to the first place, the second sorted element to second place,... So inside the adapter put the following:

public void sortByType()
{
    List<Card> sortedList = new ArrayList<>(mDiffer.getCurrentList());
    sortedList.sort(Comparator.comparing(Card::getType));
    List<Card> displayOrderList = new ArrayList<>(mDiffer.getCurrentList());
    for (int i = 0; i < sortedList.size(); ++i)
    {
        int toPos = sortedList.indexOf(displayOrderList.get(i));
        notifyItemMoved(i, toPos);
        listMoveTo(displayOrderList, i, toPos);
    }
}

private void listMoveTo(List<Card> list, int fromPos, int toPos)
{
    Card fromValue = list.get(fromPos);
    int delta = fromPos < toPos ? 1 : -1;
    for (int i = fromPos; i != toPos; i += delta) {
        list.set(i, list.get(i + delta));
    }
    list.set(toPos, fromValue);
}

and then call from activity cardsAdapter.sortByType();

3
On

I think issue is in below method

public void submitList(List<Quickcard> list) {
    
        if (list != null) {
            mListItems.submitList(list);
        }
}

because here first you need to clear old arraylist "mListItems" using

mListItems.clear();
//then add new data 
 

if (list != null) {
    mListItems.addAll(list);
}
 
//now notify adapter
notifyDataSetChanged();

Or Also you can direct notify adapter after sorting. First set adapter and pass your main list in adapter's constructor

Collections.sort(sortItems, new Comparator<Card>() {
@Override
public int compare(Card cardFirst, Card cardSecond) {
    return cardFirst.getType().compareTo(cardSecond.getType());
}

});

//now direct notify adpter
your_adapter_object.notifyDataSetChanged();
0
On

I clone the existing list mCards so the "behind the scenes" DiffUtil will see it as a different list

DiffUtil will detect changes by your implementation of DiffUtil.ItemCallback<Card>, so when you clone a card you just create a new instance of it with the same ID, therefor DiffUtil sees it as the same item because you are checking if the items are the same based on their ID:

 @Override
public boolean areItemsTheSame(@NonNull Card oldItem, @NonNull Card newItem) {

    // User properties may have changed if reloaded from the DB, but ID is fixed
    return oldItem.getId() == newItem.getId();
}

but because the cloned object's reference is different from the original item, DiffUitl will detect the items content change, because of:

@Override
public boolean areContentsTheSame(@NonNull Card oldItem, @NonNull Card newItem) {
    return oldItem.equals(newItem);
}

so DiffUtil will call adapter.notifyItemChanged(updateIndex) and will update the viewHolder in that index, but it won't change the order of items in the recyclerView.

There seems to be another problem in your sort code. You say

I would like to sort by the types/categories

but you are sorting your list alphabetically every time you click the textView. This code will give you same result every time you run it and won't change the order of recyclerView assuming the original list is sorted in the first hand. If the original list isn't sorted, clicking the textView will change the order just once.