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
}
}