72

Android Support Library 22.1 was released yesterday. Many new features were added into the v4 support library and v7, among which android.support.v7.util.SortedList<T> draws my attention.

It's said that, SortedList is a new data structure, works with RecyclerView.Adapter, maintains the item added/deleted/moved/changed animations provided by RecyclerView. It sounds like a List<T> in a ListView but seems more advanced and powerful.

So, what is the difference between SortedList<T> and List<T>? How could I use it efficiently? What's the enforcement of SortedList<T> over List<T> if it is so? Could somebody post some samples of it?

Any tips or codes will be appreciated. Thanks in advance.

SilentKnight
  • 13,761
  • 19
  • 49
  • 78
  • 2
    It must be a list that is sorted based on a Comparable implementation of the generic type T - If not, then google has lost some points in the naming convention department. – A Nice Guy Apr 22 '15 at 10:57
  • It is a list that sorts elements according to a comparison callback. So you can add elements and they will appear in the "right" position (not at the end). Take a look at the Javadoc. http://developer.android.com/reference/android/support/v7/util/SortedList.html – Thilo Apr 22 '15 at 10:57
  • 1
    Most importantly, how to use it with `RecyclerView.Adapter`? Does somebody write samples about this? – SilentKnight Apr 22 '15 at 11:04

4 Answers4

97

SortedList handles the communication to the Recycler adapter via Callback.

One difference between SortedList and List is seen in the addAll helper method in the sample below.

public void addAll(List<Page> items) {
        mPages.beginBatchedUpdates();
        for (Page item : items) {
            mPages.add(item);
        }
        mPages.endBatchedUpdates();
    }
  1. Keeps last added item

Say I have 10 cached items to load immediately when my recycler list is populated. At the same time, I query my network for the same 10 items because they could have changed since I cached them. I can call the same addAll method and SortedList will replace the cachedItems with fetchedItems under the hood (always keeps the last added item).

// After creating adapter
myAdapter.addAll(cachedItems)
// Network callback
myAdapter.addAll(fetchedItems)

In a regular List, I would have duplicates of all my items (list size of 20). With SortedList its replaces items that are the same using the Callback's areItemsTheSame.

  1. Its smart about when to update the Views

When the fetchedItems are added, onChange will only be called if one or more of the Page's title changed. You can customize what SortedList looks for in the Callback's areContentsTheSame.

  1. Its performant

If you are going to add multiple items to a SortedList, BatchedCallback call convert individual onInserted(index, 1) calls into one onInserted(index, N) if items are added into consecutive indices. This change can help RecyclerView resolve changes much more easily.

Sample

You can have a getter on your adapter for your SortedList, but I just decided to add helper methods to my adapter.

Adapter Class:

  public class MyAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private SortedList<Page> mPages;

    public MyAdapter() {
        mPages = new SortedList<Page>(Page.class, new SortedList.Callback<Page>() {
            @Override
            public int compare(Page o1, Page o2) {
                return o1.getTitle().compareTo(o2.getTitle());
            }

            @Override
            public void onInserted(int position, int count) {
                notifyItemRangeInserted(position, count);
            }

            @Override
            public void onRemoved(int position, int count) {
                notifyItemRangeRemoved(position, count);
            }

            @Override
            public void onMoved(int fromPosition, int toPosition) {
                notifyItemMoved(fromPosition, toPosition);
            }

            @Override
            public void onChanged(int position, int count) {
                notifyItemRangeChanged(position, count);
            }

            @Override
            public boolean areContentsTheSame(Page oldItem, Page newItem) {
                // return whether the items' visual representations are the same or not.
                return oldItem.getTitle().equals(newItem.getTitle());
            }

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

    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
                .inflate(R.layout.viewholder_page, parent, false);
        return new PageViewHolder(view);
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        PageViewHolder pageViewHolder = (PageViewHolder) holder;
        Page page = mPages.get(position);
        pageViewHolder.textView.setText(page.getTitle());
    }

    @Override
    public int getItemCount() {
        return mPages.size();
    }

    // region PageList Helpers
    public Page get(int position) {
        return mPages.get(position);
    }

    public int add(Page item) {
        return mPages.add(item);
    }

    public int indexOf(Page item) {
        return mPages.indexOf(item);
    }

    public void updateItemAt(int index, Page item) {
        mPages.updateItemAt(index, item);
    }

    public void addAll(List<Page> items) {
        mPages.beginBatchedUpdates();
        for (Page item : items) {
            mPages.add(item);
        }
        mPages.endBatchedUpdates();
    }

    public void addAll(Page[] items) {
        addAll(Arrays.asList(items));
    }

    public boolean remove(Page item) {
        return mPages.remove(item);
    }

    public Page removeItemAt(int index) {
        return mPages.removeItemAt(index);
    }

    public void clear() {
       mPages.beginBatchedUpdates();
       //remove items at end, to avoid unnecessary array shifting
       while (mPages.size() > 0) {
          mPages.removeItemAt(mPages.size() - 1);
       }
       mPages.endBatchedUpdates();
    }
}

Page class:

public class Page {
    private String title;
    private long id;

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }
}

Viewholder xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/text_view"
        style="@style/TextStyle.Primary.SingleLine"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />

</LinearLayout>

Viewholder class:

public class PageViewHolder extends RecyclerView.ViewHolder {
    public TextView textView;


    public PageViewHolder(View itemView) {
        super(itemView);
        textView = (TextView)item.findViewById(R.id.text_view);
    }
}
Amozoss
  • 1,445
  • 1
  • 11
  • 12
  • 1
    Is it the best way of using `SortedList`, as a field of `RecyclerView.Adapter`? – SilentKnight Apr 23 '15 at 03:10
  • I'd say its up to the programmer to decide what design works best for them. In my case, it made more sense to just have the adapter handle all the data and have the `SortedList` represent the data internally. – Amozoss Apr 23 '15 at 13:45
  • 19
    Note that there is `SortedListAdapterCallback`, which takes a `RecyclerView.Adapter` as a constructor parameter and handles the `onInserted()` and kin methods for you. – CommonsWare May 11 '15 at 15:01
  • 3
    The implementation is wrong. It breaks the contract between equals() and compareTo(). You have to check in compareTo() if the elements are equals and return 0 before comparing the titles. Else you will end up in scenarios with duplicates as the SortedList as it relies on the contract. – Paul Woitaschek Jul 26 '15 at 01:02
  • Any way to use it with a generic class? – Mukul Aug 16 '15 at 14:09
  • What if user wants to change the sort order dynamically via a setting?. How do you do that dynamically in adapter? – Raghunandan Sep 24 '15 at 08:45
  • 3
    How would you handle the case where, when updating items, I have _new_, _updated_ and **DELETED** items? `.addAll` won't help, because it doesn't remove the now deleted items and `.clear()` will "reset" the list (I need to check this). Could `.beginBatchUpdate` help, by encapsulating a loop of `.remove()` followed by an `.addAll()`? – David Corsalini Oct 22 '15 at 16:10
  • @PaulWoitaschek is right, I ended up with duplicate items. How did you solve the duplicate issue? – Otieno Rowland Dec 25 '15 at 15:15
  • @PaulWoitaschek I have the same issue with duplicates, did you find the solution? – Igori S Oct 13 '16 at 14:46
14

SortedList is in v7 support library.

A SortedList implementation that can keep items in order and also notify for changes in the list such that it can be bound to a RecyclerView.Adapter.

It keeps items ordered using the compare(Object, Object) method and uses binary search to retrieve items. If the sorting criteria of your items may change, make sure you call appropriate methods while editing them to avoid data inconsistencies.

You can control the order of items and change notifications via the SortedList.Callback parameter.

Here below is a sample of use of SortedList, I think it is what you want, take a look at it and enjoy!

public class SortedListActivity extends ActionBarActivity {
    private RecyclerView mRecyclerView;
    private LinearLayoutManager mLinearLayoutManager;
    private SortedListAdapter mAdapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.sorted_list_activity);
        mRecyclerView = (RecyclerView) findViewById(R.id.recycler_view);
        mRecyclerView.setHasFixedSize(true);
        mLinearLayoutManager = new LinearLayoutManager(this);
        mRecyclerView.setLayoutManager(mLinearLayoutManager);
        mAdapter = new SortedListAdapter(getLayoutInflater(),
                new Item("buy milk"), new Item("wash the car"),
                new Item("wash the dishes"));
        mRecyclerView.setAdapter(mAdapter);
        mRecyclerView.setHasFixedSize(true);
        final EditText newItemTextView = (EditText) findViewById(R.id.new_item_text_view);
        newItemTextView.setOnEditorActionListener(new TextView.OnEditorActionListener() {
            @Override
            public boolean onEditorAction(TextView textView, int id, KeyEvent keyEvent) {
                if (id == EditorInfo.IME_ACTION_DONE &&
                        (keyEvent == null || keyEvent.getAction() == KeyEvent.ACTION_DOWN)) {
                    final String text = textView.getText().toString().trim();
                    if (text.length() > 0) {
                        mAdapter.addItem(new Item(text));
                    }
                    textView.setText("");
                    return true;
                }
                return false;
            }
        });
    }

    private static class SortedListAdapter extends RecyclerView.Adapter<TodoViewHolder> {
        SortedList<Item> mData;
        final LayoutInflater mLayoutInflater;
        public SortedListAdapter(LayoutInflater layoutInflater, Item... items) {
            mLayoutInflater = layoutInflater;
            mData = new SortedList<Item>(Item.class, new SortedListAdapterCallback<Item>(this) {
                @Override
                public int compare(Item t0, Item t1) {
                    if (t0.mIsDone != t1.mIsDone) {
                        return t0.mIsDone ? 1 : -1;
                    }
                    int txtComp = t0.mText.compareTo(t1.mText);
                    if (txtComp != 0) {
                        return txtComp;
                    }
                    if (t0.id < t1.id) {
                        return -1;
                    } else if (t0.id > t1.id) {
                        return 1;
                    }
                    return 0;
                }

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

                @Override
                public boolean areItemsTheSame(Item item1, Item item2) {
                    return item1.id == item2.id;
                }
            });
            for (Item item : items) {
                mData.add(item);
            }
        }

        public void addItem(Item item) {
            mData.add(item);
        }

        @Override
        public TodoViewHolder onCreateViewHolder(final ViewGroup parent, int viewType) {
            return new TodoViewHolder (
                    mLayoutInflater.inflate(R.layout.sorted_list_item_view, parent, false)) {
                @Override
                void onDoneChanged(boolean isDone) {
                    int adapterPosition = getAdapterPosition();
                    if (adapterPosition == RecyclerView.NO_POSITION) {
                        return;
                    }
                    mBoundItem.mIsDone = isDone;
                    mData.recalculatePositionOfItemAt(adapterPosition);
                }
            };
        }

        @Override
        public void onBindViewHolder(TodoViewHolder holder, int position) {
            holder.bindTo(mData.get(position));
        }

        @Override
        public int getItemCount() {
            return mData.size();
        }
    }

    abstract private static class TodoViewHolder extends RecyclerView.ViewHolder {
        final CheckBox mCheckBox;
        Item mBoundItem;
        public TodoViewHolder(View itemView) {
            super(itemView);
            mCheckBox = (CheckBox) itemView;
            mCheckBox.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {
                @Override
                public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
                    if (mBoundItem != null && isChecked != mBoundItem.mIsDone) {
                        onDoneChanged(isChecked);
                    }
                }
            });
        }

        public void bindTo(Item item) {
            mBoundItem = item;
            mCheckBox.setText(item.mText);
            mCheckBox.setChecked(item.mIsDone);
        }

        abstract void onDoneChanged(boolean isChecked);
    }

    private static class Item {
        String mText;
        boolean mIsDone = false;
        final public int id;
        private static int idCounter = 0;

        public Item(String text) {
            id = idCounter ++;
            this.mText = text;
        }
    }
}
Richard Le Mesurier
  • 29,432
  • 22
  • 140
  • 255
SilentKnight
  • 13,761
  • 19
  • 49
  • 78
  • Sorry for commenting on this after months, but, am I the only who's experiencing some serious problems when adding a few items causing the recycling? If I add , say, 12 items it goes crazy and starts triggering some movements, plus, it deletes all previously entered data... – fiipi Dec 23 '15 at 16:35
  • Sorry, I meant the sample codes, just let me elaborate on that a bit. Maybe it's better to ask a separate question. – fiipi Dec 24 '15 at 15:54
  • TL;DR: use `SortedListAdapterCallback` when you construct your `SortedList<>` – lionello Dec 18 '16 at 08:06
5

There is a sample SortedListActivity in the support library source repository which demonstrates how to use SortedList and SortedListAdapterCallback inside a RecyclerView.Adapter. From the root of the SDK, with the support library installed, it should be at extras/android/support/samples/Support7Demos/src/com/example/android/supportv7/util/SortedListActivity.java (also on github).

The existence of these particular samples is mentioned exactly once in Google's documentation, at the bottom of a page dealing with a different topic, so I don't blame you for not finding it.

moskvax
  • 126
  • 2
  • 4
2

About SortedList implementation, it is backed by an array of <T> with a default min capacity of 10 items. Once the array is full the array is resized to size() + 10

The source code is available here

From documentation

A Sorted list implementation that can keep items in order and also notify for changes in the list such that it can be bound to a RecyclerView.Adapter.

It keeps items ordered using the compare(Object, Object) method and uses binary search to retrieve items. If the sorting criteria of your items may change, make sure you call appropriate methods while editing them to avoid data inconsistencies.

You can control the order of items and change notifications via the SortedList.Callback parameter.

Regarding to performance they also added SortedList.BatchedCallback to carry out multiple operation at once instead of one at the time

A callback implementation that can batch notify events dispatched by the SortedList.

This class can be useful if you want to do multiple operations on a SortedList but don't want to dispatch each event one by one, which may result in a performance issue.

For example, if you are going to add multiple items to a SortedList, BatchedCallback call convert individual onInserted(index, 1) calls into one onInserted(index, N) if items are added into consecutive indices. This change can help RecyclerView resolve changes much more easily.

If consecutive changes in the SortedList are not suitable for batching, BatchingCallback dispatches them as soon as such case is detected. After your edits on the SortedList is complete, you must always call dispatchLastEvent() to flush all changes to the Callback.

Austyn Mahoney
  • 11,398
  • 8
  • 64
  • 85
Axxiss
  • 4,759
  • 4
  • 26
  • 45