0

I wrote a wrapper around FirestorePagingAdapter. This works fine most of the times. But there are occasions where this crashes with

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter

I will show you the complete wrapper. Also the last log message I see before the crash is

[Paging adapter] Data loading finished.

I also noticed, when I slowly scroll the list it works fine. Only if I scroll the list fast it crashes eventually.

So here is the code. I cannot figure out where the problem is. Any help is highly appreciated

public abstract class PagingAdapter<T extends RecyclerItem> extends FirestorePagingAdapter<T, RecyclerViewHolder<T, ? extends ViewBinding>> implements Function1<CombinedLoadStates, Unit> {
protected final String TAG = this.getClass().getSimpleName();
private final SnapshotParser<T> mParser;
private SortedList<T> mListItems;

private int mTryCount;
private boolean mReverseFill = false;


private PagingAdapter(@NonNull FirestorePagingOptions<T> options, PagingAdapterCallback<T> callback) {
    super(options);

        mListItems = new SortedList(RecyclerItem.class, new SortedListAdapterCallback<T>(this) {
            @Override
            public int compare(T o1, T o2) {
                return callback.compare(o1, o2);
            }

            @Override
            public boolean areContentsTheSame(T oldItem, T newItem) {
                return callback.areContentsTheSame(oldItem, newItem);
            }

            @Override
            public boolean areItemsTheSame(T item1, T item2) {
                return callback.areContentsTheSame(item1, item2);
            }
        });

    mParser = options.getParser();
    this.mTryCount = 0;
}

public void setReverseFill(boolean reverseFill) {
    this.mReverseFill = reverseFill;
}

@Override
public int getItemViewType(int position) {
    return getList().get(position).getRecyclerItemType().getId();
}

public SortedList<T> getList() {
    return mListItems;
}


@NonNull
@Override
public RecyclerViewHolder<T, ? extends ViewBinding> onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
    RecyclerViewHolder<T, ? extends ViewBinding> viewHolder = onCreateViewHolder(parent, RecyclerItemType
            .get(viewType));
    if (viewHolder == null) {
        throw new NullPointerException("Your list contains items for that you did not specify a view holder for");
    }
    return viewHolder;
}

public abstract RecyclerViewHolder<T, ? extends ViewBinding> onCreateViewHolder(@NonNull ViewGroup parent, RecyclerItemType itemType);

@Override
public void onBindViewHolder(RecyclerViewHolder<T, ? extends ViewBinding> holder, int position) {
    try {
        //and trigger the paging to load around with the correct "position"
        super.onBindViewHolder(holder, getPagingPosition(position)); //<--Needed to page the list, be we do not really use it (see below)
    } catch (Exception ignore) {
        //If this fails, because there are less items in the list, do not crash, this is fine
        Log.d(TAG, "onBindViewHolder: ");
    }
    holder.bind(getList().get(position));
}

protected int getPagingPosition(int requestedPosition) {
    return requestedPosition;
}

@Override
protected void onBindViewHolder(@NonNull RecyclerViewHolder<T, ? extends ViewBinding> holder, int position, @NonNull T model) {
    //We do not use this method
}

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

@Override
public Unit invoke(CombinedLoadStates states) {
    LoadState refresh = states.getRefresh();
    LoadState append = states.getAppend();

    if (refresh instanceof LoadState.Error || append instanceof LoadState.Error) {
        //The previous load (either initial or additional) failed
        Log.d(TAG, "[Paging adapter] An error occurred while loading the data");
        if (mTryCount < 3) {
            mTryCount += 1;
            retry();
        } 
    }

    if (refresh instanceof LoadState.Loading) {
        Log.d(TAG, "[Paging adapter] Loading initial data");
    }

    if (append instanceof LoadState.Loading) {
        Log.d(TAG, "[Paging adapter] Loading more data");   
    }

    if (append instanceof LoadState.NotLoading) {
        LoadState.NotLoading notLoading = (LoadState.NotLoading) append;
        if (notLoading.getEndOfPaginationReached()) {
            Log.d(TAG, "[Paging adapter] No further documents");
            mTryCount = 0;
            return null;
        }

        if (refresh instanceof LoadState.NotLoading) {
            Log.d(TAG, "[Paging adapter] Data loading finished");
            mTryCount = 0;
            List<T> items = new ArrayList<>();
            if (mReverseFill) {
                for (int i = snapshot().size() - 1; i >= 0; i--) {
                    T currentItem = mParser.parseSnapshot(snapshot().get(i));
                    if (getList().indexOf(currentItem) == SortedList.INVALID_POSITION)
                        items.add(currentItem);
                }
            } else {
                for (DocumentSnapshot snapshot : snapshot()) {
                    T currentItem = mParser.parseSnapshot(snapshot);
                    if (getList().indexOf(currentItem) == SortedList.INVALID_POSITION)
                        items.add(currentItem);
                }
            }
            addAll(items);
            return null;
        }
    }
    return null;
}

public void addAll(Collection<T> items) {
    getList().beginBatchedUpdates();
    getList().addAll(items);
    getList().endBatchedUpdates();
}


public abstract static class PagingAdapterCallback<T extends RecyclerItem> {
    public abstract int compare(T o1, T o2);

    public abstract boolean areContentsTheSame(T oldItem, T newItem);

    public abstract boolean areItemsTheSame(T item1, T item2);
}
}

Just in case you wonder. These are the other two wrapper:

public abstract class RecyclerViewHolder<T extends RecyclerItem, E extends ViewBinding> extends RecyclerView.ViewHolder {
public final String TAG = this.getClass().getSimpleName();
protected final E b;
private final Context mContext;

protected RecyclerViewHolder(@NonNull E b) {
    super(b.getRoot());
    this.b = b;
    this.mContext = b.getRoot().getContext();
}

public abstract void bind(T item);

protected Context getContext() {
    return mContext;
}

protected Context requireContext() {
    return getContext();
}

protected String getString(int resId) {
    return mContext.getString(resId);
}

protected String getString(int resId, Object... args) {
    return mContext.getString(resId, args);
}

}

and

public interface RecyclerItem {

 @NonNull
 RecyclerItemType getRecyclerItemType();

 default void setRecyclerItemType(RecyclerItemType type){
      //Does not do anything per default
 }

}
Avinta
  • 678
  • 1
  • 9
  • 26
  • If you encounter problems, it's best to create a [MCVE](https://stackoverflow.com/help/mcve) when posting a question. You posted almost **300 (three hundred)** lines of code for this issue. That's a lot for people to parse and try to debug online. Please edit your question and isolate the problem, in that way you increase your chances of being helped. – Alex Mamo Dec 20 '21 at 09:51
  • Yes. Normally I would. But in this case I cannot really narrow in the problem as it only occurs occasionally. I really don't have any idea why. But I will try to investigate myself. Just hoped it might be obvious to someone directly (I do weird stuff in onBindViewHolder which might be causing this issue, so I will start looking there). If I can narrow it down, I will of course update the question with more precise information. But I will - for now - delete definetly unecessary lines – Avinta Dec 20 '21 at 12:04
  • Did you have the chance to have a look at this SO [post](https://stackoverflow.com/questions/31759171/recyclerview-and-java-lang-indexoutofboundsexception-inconsistency-detected-in) and this other [one](https://stackoverflow.com/questions/35653439/recycler-view-inconsistency-detected-invalid-view-holder-adapter-positionviewh/44590192) ? – Sergi Dec 21 '21 at 17:57
  • @Sergi yes aktually I saw those, I did not try this (I guess it works - I will use it as a workaround for now if it works) - But effectively WE shouldn't solve this like this because the exception is thrown with reason. – Avinta Dec 21 '21 at 19:00
  • I now implemented the wrapper class. It stops crashing BUT whenever it should crash the recycler view stops scrolling, the last item in the list flickers (like it is updating) and after we can continue scrolling. So this 'solution' is not really acceptable. I am using a SortedList so I do not call any notify methods myself as you can see. Any idea how to bug fix the root cause? – Avinta Dec 21 '21 at 19:20
  • So I have more news in case this might help to figure out what is happening. The Error occurs whenever I scoll faster then the backend can load around the data from firebase. So when I reach the end of the list. It also only happens if I use "DefaultItemAnimator" in the adapter. If I set the Animator to null the exception is not thrown. This is basically fine with me now, but I would like to know what is causign this (Maybe I need animations in the future) – Avinta Dec 22 '21 at 09:37
  • I understand this is not the best fix. I believe that to find the cause of this, deeper troubleshooting may be needed. For example, does it work correctly if inside your `onBindViewHolder` you try first checking `if(position != RecyclerView.NO_POSITION)` and doing the binding inside the if? – Sergi Dec 28 '21 at 15:27

0 Answers0