I'm implementing a SectionedRecyclerViewAdapter
. I know that there are many great libraries for this task, but before quit I'd like to enjoy the try. So please, before shoot at me consider that this is a prototype and every suggestion about improvements will be welcome.
Here the implementation. The concept is simple, every pojo
that wants to be sectioned must implement the Sectioned
interface with just one method that return the title of section. Then a LinkedHashMap
will keep track of the sections vs indices.
import android.support.v7.widget.RecyclerView;
import android.util.Log;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
abstract
public class SectionedRecyclerViewAdapter<
HeaderViewHolder extends RecyclerView.ViewHolder,
ValueViewHolder extends RecyclerView.ViewHolder>
extends
RecyclerView.Adapter<RecyclerView.ViewHolder> {
public interface Sectioned {
String getSection();
}
static final private String TAG = "sectioned-rv";
private static final int TYPE_NORMAL = 1;
private static final int TYPE_HEADER = 0;
final
private List<Sectioned> mObjects;
final
private Object mLock;
final
private LinkedHashMap<Integer, String> sectionsIndexer;
/* Removed for now, not sure if will be more elegant/better or worst
private final RecyclerView.AdapterDataObserver mDataSetObserver = new RecyclerView.AdapterDataObserver() {
@Override
public void onChanged() {
calculateSectionHeaders();
}
@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
onChanged();
}
};
*/
private boolean mNotifyOnChange = true;
public SectionedRecyclerViewAdapter() {
super();
mLock = new Object();
sectionsIndexer = new LinkedHashMap<>();
mObjects = new ArrayList<>();
setHasStableIds(true);
}
public void changeData(List<? extends Sectioned> data) {
synchronized (mLock) {
mObjects.clear();
if (data != null)
mObjects.addAll(data);
calculateSectionHeaders();
}
if (mNotifyOnChange) notifyDataSetChanged();
}
public void removeItemAt(int position) {
int viewType = getItemViewType(position);
if (viewType == TYPE_NORMAL) {
int index = getSectionForPosition(position);
synchronized (mLock) {
mObjects.remove(index);
calculateSectionHeaders();
}
if (mNotifyOnChange) notifyItemRemoved(index); //<- Crash
// if (mNotifyOnChange) notifyDataSetChanged(); //<- Fine
}
}
public void clear() {
synchronized (mLock) {
mObjects.clear();
sectionsIndexer.clear();
}
if (mNotifyOnChange) notifyDataSetChanged();
}
public void sort(Comparator<? super Sectioned> comparator) {
synchronized (mLock) {
Collections.sort(mObjects, comparator);
calculateSectionHeaders();
}
if (mNotifyOnChange) notifyDataSetChanged();
}
/**
* Interceptor before the sections are calculated so we can transform some computer data into human readable,
* e.g. format a unix timestamp, or a status
*
* By default this method returns the original data for the group.
* @param groupData original group name
* @return the transformed group name
*/
protected String getCustomGroup(String groupData) {
return groupData;
}
@Override
public int getItemCount() {
return mObjects.size() + sectionsIndexer.size();
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
if (viewType == TYPE_HEADER) {
return onCreateHeaderViewHolder(parent);
}
return onCreateValueViewHolder(parent);
}
@SuppressWarnings("unchecked")
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
int viewType = getItemViewType(position);
if (viewType == TYPE_NORMAL) {
Sectioned item = getItem(position);
if (item == null) {
Log.v(TAG, "getItem(" + position + ") = null");
return;
}
onBindValueViewHolder((ValueViewHolder)holder, position);
}
else {
final String group = sectionsIndexer.get(position);
onBindHeaderViewHolder(group, (HeaderViewHolder) holder);
}
}
public Sectioned getItem(int position) {
if (getItemViewType(position) == TYPE_NORMAL) {
return mObjects.get(getSectionForPosition(position));
}
return mObjects.get(position);
}
@Override
public long getItemId(int position) { return position; }
@Override
public int getItemViewType(int position) {
if (position == getPositionForSection(position)) {
return TYPE_NORMAL;
}
return TYPE_HEADER;
}
abstract
protected HeaderViewHolder onCreateHeaderViewHolder(ViewGroup parent);
abstract
protected void onBindHeaderViewHolder(String group, HeaderViewHolder viewHolder);
abstract
protected ValueViewHolder onCreateValueViewHolder(ViewGroup parent);
abstract
protected void onBindValueViewHolder(ValueViewHolder holder, int position);
private int getPositionForSection(int section) {
if (sectionsIndexer.containsKey(section)) {
return section + 1;
}
return section;
}
private int getSectionForPosition(int position) {
int offset = 0;
for (Integer key : sectionsIndexer.keySet()) {
if (position > key) {
offset++;
} else {
break;
}
}
return position - offset;
}
private void calculateSectionHeaders() {
int i = 0;
String previous = "";
int count = 0;
sectionsIndexer.clear();
for (Sectioned item: mObjects) {
final String group = getCustomGroup(item.getSection());
if (!previous.equals(group)) {
sectionsIndexer.put(i + count, group);
previous = group;
Log.v(TAG, "Group <" + group + "> at position: " + (i + count));
count++;
}
i++;
}
}
}
Everything works fine but I noticed that when I remove an item calling removeItemAt(position)
If I call notifyItemRemoved(index);
the app will crash with this exception:
java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{855a1d3 position=3 id=4, oldPos=4, pLpos:4 scrap [attachedScrap] tmpDetached no parent}
at android.support.v7.widget.RecyclerView$Recycler.validateViewHolderForOffsetPosition(RecyclerView.java:5041) at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5172)
at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5153)
If I call notifyDataSetChanged();
after removing the item, everything works fine.
So I'm just curious why this happens. Debugging with breakpoints to RecyclerView implementation I noticed that the cause could be the check on:
holder.mPosition >= mAdapter.getItemCount()
but cannot see why the check fails if in my implementation getItemCount
return this:
@Override
public int getItemCount() {
return mObjects.size() + sectionsIndexer.size();
}
Thank you very much