0

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

Luca Sepe
  • 2,435
  • 1
  • 20
  • 26
  • 1
    May help you http://stackoverflow.com/questions/30220771/recyclerview-inconsistency-detected-invalid-item-position – Praveena Nov 10 '16 at 10:03
  • Hi @Praveen thanks but that looks like as another type of "Inconsistency". Thansk anyway. – Luca Sepe Nov 10 '16 at 10:13
  • @Luca Sepe What is the benefit of using "synchronized (mLock)" for CRUD operations in the adapter? I haven't come across its usage before and am wondering what the benefits are. – AJW Jan 07 '22 at 17:29

0 Answers0