12

I have some problems understanding RecyclerViews SortedList.

Lets say I have a very simple class only having a very simple class holding data:

public class Pojo {
    public final int id;
    public final char aChar;

    public Pojo(int id, char aChar) {
        this.id = id;
        this.aChar = aChar;
    }

    @Override
    public String toString() {
        return "Pojo[" + "id=" + id
                + ",aChar=" + aChar
                + "]";
    }
}

My understanding is that the sorted list won't contain any duplicates.

But when I have a SortedList with callbacks like this:

....

@Override
public boolean areContentsTheSame(Pojo oldItem, Pojo newItem) {
    return oldItem.aChar == newItem.aChar;
}

@Override
public int compare(Pojo o1, Pojo o2) {
    return Character.compare(o1.aChar, o2.aChar);
}

@Override
public boolean areItemsTheSame(Pojo item1, Pojo item2) {
    return item1.id == item2.id;
}

I end up with duplicates when I add multiple items with the same id but different chars.

sortedList.add(new Pojo(1, 'a'));
sortedList.add(new Pojo(1, 'b'));

I would expect the list to update the item. Instead now I have multiple items even though areItemsTheSame returned true.

Paul Woitaschek
  • 6,717
  • 5
  • 33
  • 52
  • List can have duplicates. Try using hashMap. – Anitha Manikandan Aug 06 '15 at 10:50
  • From my understanding not ["If the item already exists in the list and its sorting criteria is not changed, it is replaced with the existing Item."](https://developer.android.com/reference/android/support/v7/util/SortedList.html#add(T)). I dont need a HashMap as I need the efficient coupling with RecyclerView. – Paul Woitaschek Aug 06 '15 at 10:53
  • Why this question is tagged with a Java tag? – Boris Aug 06 '15 at 11:21
  • @Paul Woitaschek did you manage to solve this problem? – Ari Nov 10 '15 at 16:07
  • 3
    Yes @Ari 'm checking it manually. See [here](https://github.com/Ph1b/MaterialAudiobookPlayer/blob/v2.4.2/audiobook/src/main/java/de/ph1b/audiobook/adapter/BookShelfAdapter.java#L88) – Paul Woitaschek Nov 10 '15 at 16:22

6 Answers6

9

SortedList does not keep any mapping by ids (because there are no ids in the API). So when the sorting criteria changes (a to b in your case), SortedList cannot find the existing element.

You can keep the id mapping yourself, then have your add method as follows:

void add(Item t) {
  Item existing = idMap.get(t.id);
  if (existing == null) {        
     sortedList.add(t);
  } else {
     sortedList.updateItemAt(sortedList.indexOf(existing), t);
  }
  idMap.put(t.id, t);
}

You'll also need to implement a remove method to remove the item from the idMap.

yigit
  • 37,683
  • 13
  • 72
  • 58
  • As you pointed out, when sort criteria changed, the SortedList cannot find the existing element, how could `indexOf` find out the correct index? In my case, it always return `-1`. – Shaw Mar 30 '16 at 13:19
  • you need to call it "before" changing the data. – yigit Mar 31 '16 at 04:57
5

As Minhtdh already mentioned in his answer, the problem lies within your compare().

See, add() looks up the existing object's index by using the compare() you implement. Therefore when your compare() returns something other than 0 it adds the object to the list.

You would need to check if the items are the same before comparing it's contents. However, if your content can be the same you would need a secondary comparison.

This is how I would implement the compare() in your case:

@Override
public int compare(Pojo o1, Pojo o2) {
    int result;
    if (areItemsTheSame(o1, o2) {
        result = 0;
    } else {
        result = Character.compare(o1.aChar, o2.aChar);
        if (result == 0) {
            // TODO implement a secondary comparison 
        }
    }

    return result;
}
robocab
  • 777
  • 5
  • 15
0

I think you should use Integer.compare(o1.id, o2.id); in compare method, it's where SortList decide those 2 items are the same or not.

justHooman
  • 3,044
  • 2
  • 17
  • 15
  • 1
    From the doc it says clearly that that is for the ordering. And I would think that if > 2 items are the same or not is decided in `areItemsTheSame()` – Paul Woitaschek Aug 06 '15 at 11:26
  • Well, I think those 2 method `compare` and `areItemsTheSame` like `hashCode` and `equal`, If you want 2 objects have same identity, they must have same `hashCode` first, then `equal` return true. So in this case, `compare` == 0 must come first, then `areItemsTheSame` is true later. – justHooman Aug 06 '15 at 14:11
0

You can check if the object already exist in the sorted list by doing this.

if (sortedList.indexOf(item) == -1) 
{
    sortedList.add(item);  //Item still does not exist because index is -1
} 
else 
{
    sortedList.updateItemAt(sortedList.indexOf(item), item);
}
Farhad
  • 4,119
  • 8
  • 43
  • 66
gienapps
  • 1
  • 1
0

I had a similar issue when creating a chat app which I need to update messages by their IDs and sort them by their dates. The support lib's SortedList does not do that or at least I had little time to dive into its source code and test. So, I created a small component, MultiSortedList:

import android.support.v7.widget.RecyclerView

/**
 * Created by abduaziz on 6/14/18.
 *
 *   MultiSortedList is a wrapper component to ArrayList that keeps its elements in a sorted order
 *   using UpdateCallbackInterface. It is intended to be used inside recycler view adapters.
 *
 * */

class MultiSortedList<T>(var updateCallback: UpdateCallback<T>, var adapter: RecyclerView.Adapter<*>? = null) {

    companion object {
        val TAG = "SORTEDLIST"
    }

    // internal list to hold elements by sortBy() -> visible to user
    private val list: ArrayList<T> = arrayListOf()

    // internal list to hold elements by updateBy() -> not visible
    private val uList: ArrayList<T> = arrayListOf()

    // add adapter from ui
    fun addAdapter(adapter: RecyclerView.Adapter<*>?) {
        this.adapter = adapter
    }

    /*
    * 1. Search for existing element that satisfies updateBy()
    * 2. Remove the existing element if found
    * 3. Add the new item with sortBy()
    * 4. Notify if adapter is not null
    * */
    fun add(newItem: T) {
        remove(newItem)

        // save to internal list by updateBy()
        var toBeStoredPosition = uList.binarySearch { updateCallback.updateBy(it, newItem) }
        if (toBeStoredPosition < 0) toBeStoredPosition = -(toBeStoredPosition + 1)
        uList.add(toBeStoredPosition, newItem)

        // save to UI list and notify changes
        var sortPosition = list.binarySearch { updateCallback.sortBy(it, newItem) }
        if (sortPosition < 0) sortPosition = -(sortPosition + 1)
        list.add(sortPosition, newItem)
        adapter?.notifyItemInserted(sortPosition)
    }

    /*
    * Remove and notify the adapter
    * */
    fun remove(removeItem: T) {
        val storedElementPosition = uList.binarySearch { updateCallback.updateBy(it, removeItem) }
        if (storedElementPosition >= 0 && storedElementPosition < uList.size) {

            // remove from internal list
            val itemTobeRemoved = uList[storedElementPosition]
            uList.removeAt(storedElementPosition)

            // remove from ui
            val removePosition = list.binarySearch { updateCallback.sortBy(it, itemTobeRemoved) }
            if (removePosition >= 0 && removePosition < list.size) {
                list.removeAt(removePosition)
                adapter?.notifyItemRemoved(removePosition)
            }
        }
    }

    // can be accessed -> list.get(position) or list[position]
    operator fun get(pos: Int): T {
        return list[pos]
    }

    // for adapter use
    fun size(): Int {
        return list.size
    }

    inline fun forEachIndexed(action: (Int, T) -> Unit) {
        for (index in 0 until size()) {
            action(index, get(index))
        }
    }

    /*
    * UpdateCallback is the main interface that is used to compare the elements.
    *   - sortBy() is used to locate new elements passed to SortedList
    *   - updateBy() is used to update/remove elements
    *
    * Typical example would be Message model class which we want to:
    *   - Sort messages according to their dates
    *   - Update/Remove messages according to their randomIDs or IDs.
    * */
    interface UpdateCallback<T> {
        fun sortBy(i1: T, i2: T): Int
        fun updateBy(oldItem: T, newItem: T): Int
    }
}

The usage is explained here: https://medium.com/@abduazizkayumov/sortedlist-with-recyclerview-part-2-64c3e9b1b124

Abduaziz Kayumov
  • 133
  • 2
  • 10
-1

In Java a collection that contains no duplicate elements is Set. The common Implementing Classes are HashSet and TreeSet. You are wrong assuming that SortedList does that.

Boris
  • 22,667
  • 16
  • 50
  • 71
  • But doesn't [this](https://developer.android.com/reference/android/support/v7/util/SortedList.html#add(T)) imply that? – Paul Woitaschek Aug 06 '15 at 11:05
  • Yes, it looks confusing to me. Normally List can contain duplicates, but Set not. Can you try to use SortedSet instead of SortedList? – Boris Aug 06 '15 at 11:14
  • 1
    Its an android SortedList, not utils.java. There is no SortedSet for binding with recyclerview. – Paul Woitaschek Aug 06 '15 at 11:15