Search code examples
androidsortingandroid-recyclerviewandroid-asynclistdiffer

Android how to sort RecyclerView List when using AsyncListDiffer?


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;
}  

Solution

  • 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();