3

Currently I tried to use DiffUtil to notify the items of RecyclerView in my project.

Meanwhile, I wrote a custom adapter which could add headers and footers. I used this adapter and added a load more footer so that my list could load more when scrolling down. This lead to a point: my list which could load more will always hold at least one item (load more footer).

To avoid misunderstanding, the word "items" below will specifically mean the items are not headers or footers.

Then the problem occurred when I notify items by diffUtil from n (n > 0) items to zero, my app crashes.

Here is the exception: java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder

It is worth mentioning that if I use the custom adapter without headers or footers, everything will be okay.

I have searched for solutions but none of their situations are the same to me.

Here are the codes(Java) of custom adapter, diffUtil is dispatched updates to the innerAdapter in it:

public class WrapperAdapter extends RecyclerView.Adapter {

    // 0x40000000 为 1位 flag 位,1为 HEADER ,0 为 FOOTER
    private static final int HEADER_FOOTER_TYPE_MASK = 0x40000000;
    // 0x3f0000 为 6位 index 位,对应的 HEADER FOOTER 数组的 index ,也就是说最多保存64个 HEADER 、64个 FOOTER
    private static final int HEADER_FOOTER_INDEX_MASK = 0x3f000000;
    // 后面的 24 位为 view 的 hash code 位,可以保证每个 HEADER、FOOTER 都能有不同的 viewType
    private static final int HEADER_FOOTER_HASH_MASK = 0x00ffffff;

    private RecyclerView.Adapter innerAdapter;
    private RecyclerView.AdapterDataObserver innerObserver = new AdapterDataObserverProxy();
    private List<View> headers = new ArrayList<>();
    private List<View> footers = new ArrayList<>();

    public WrapperAdapter(@NonNull RecyclerView.Adapter adapter) {
        innerAdapter = adapter;
    }

    @NonNull
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        if (viewType < 0) {
            if ((viewType & HEADER_FOOTER_TYPE_MASK) != 0) {
                // HEADER
                int headerIndex = (viewType & HEADER_FOOTER_INDEX_MASK) >> 24;
                return new InnerViewHolder(headers.get(headerIndex));
            } else {
                // FOOTER
                int footerIndex = (viewType & HEADER_FOOTER_INDEX_MASK) >> 24;
                return new InnerViewHolder(footers.get(footerIndex));
            }
        }
        return innerAdapter.onCreateViewHolder(parent, viewType);
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
        if (isHeader(position) || isFooter(position)) {
            return;
        }

        innerAdapter.onBindViewHolder(holder, position - headers.size());
    }

    @Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) {
        if (isHeader(position) || isFooter(position)) {
            return;
        }

        innerAdapter.onBindViewHolder(holder, position - headers.size(), payloads);
    }

    @Override
    public int getItemViewType(int position) {
        if (isHeader(position))  {
            return Integer.MIN_VALUE | HEADER_FOOTER_TYPE_MASK | ((position & (HEADER_FOOTER_INDEX_MASK >> 24)) << 24) | (headers.get(position).hashCode() & HEADER_FOOTER_HASH_MASK);
        }

        if (isFooter(position)) {
            int footerIndex = position - innerAdapter.getItemCount() - headers.size();
            return Integer.MIN_VALUE | ((footerIndex & (HEADER_FOOTER_INDEX_MASK >> 24)) << 24) | (footers.get(footerIndex).hashCode() & HEADER_FOOTER_HASH_MASK);
        }

        int innerViewType = innerAdapter.getItemViewType(position - headers.size());
        if (innerViewType < 0) {
            throw new IllegalArgumentException("View type cannot be negative, which is claimed by HEADER and FOOTER");
        }
        return innerViewType;
    }

    @Override
    public int getItemCount() {
        if (innerAdapter.getItemCount() == 0) {
            return headers.size();
        } else {
            return innerAdapter.getItemCount() + headers.size() + footers.size();
        }
    }

    private boolean isHeader(int position) {
        return position < headers.size();
    }

    private boolean isFooter(int position) {
        return position > getItemCount() - footers.size() - 1;
    }

    private class InnerViewHolder extends RecyclerView.ViewHolder {
        InnerViewHolder(@NonNull View itemView) {
            super(itemView);
        }
    }

    public void addHeader(@NonNull View header) {
        if (!headers.contains(header)) {
            headers.add(header);
            notifyItemInserted(headers.size() - 1);
        }
    }

    public void removeHeader(@NonNull View header) {
        if (headers.contains(header)) {
            int index = headers.indexOf(header);
            headers.remove(index);
            notifyItemRemoved(index);
        }
    }

    public void addFooter(@NonNull View footer) {
        if (!footers.contains(footer)) {
            footers.add(footer);
            notifyItemInserted(getItemCount() - 1);
        }
    }

    public void removeFooter(@NonNull View footer) {
        if (footers.contains(footer)) {
            int index = footers.indexOf(footer);
            footers.remove(index);
            notifyItemRemoved(headers.size() + innerAdapter.getItemCount() + index);
        }
    }


    @Override
    public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            super.onViewRecycled(holder);
        } else {
            innerAdapter.onViewRecycled(holder);
        }
    }

    @Override
    public boolean onFailedToRecycleView(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            return super.onFailedToRecycleView(holder);
        } else {
            return innerAdapter.onFailedToRecycleView(holder);
        }

    }

    @Override
    public void onViewAttachedToWindow(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            super.onViewAttachedToWindow(holder);
        } else {
            innerAdapter.onViewAttachedToWindow(holder);
        }
    }

    @Override
    public void onViewDetachedFromWindow(@NonNull RecyclerView.ViewHolder holder) {
        if (holder instanceof InnerViewHolder) {
            super.onViewDetachedFromWindow(holder);
        } else {
            innerAdapter.onViewDetachedFromWindow(holder);
        }
    }

    @Override
    public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
        super.registerAdapterDataObserver(observer);
        innerAdapter.registerAdapterDataObserver(innerObserver);
    }

    @Override
    public void unregisterAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
        super.unregisterAdapterDataObserver(observer);
        innerAdapter.unregisterAdapterDataObserver(innerObserver);
    }

    @Override
    public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) {
        innerAdapter.onAttachedToRecyclerView(recyclerView);
    }

    @Override
    public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
        innerAdapter.onDetachedFromRecyclerView(recyclerView);
    }

    @Override
    public void setHasStableIds(boolean hasStableIds) {
        super.setHasStableIds(hasStableIds);
        innerAdapter.setHasStableIds(hasStableIds);
    }

    @Override
    public long getItemId(int position) {
        if (isHeader(position)) return super.getItemId(position);
        if (isFooter(position)) return super.getItemId(position);
        return innerAdapter.getItemId(position);
    }

    private class AdapterDataObserverProxy extends RecyclerView.AdapterDataObserver {
        @Override
        public void onChanged() {
            WrapperAdapter.this.notifyDataSetChanged();
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount) {
            WrapperAdapter.this.notifyItemRangeChanged(positionStart + WrapperAdapter.this.headers.size(), itemCount);
        }

        @Override
        public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
            WrapperAdapter.this.notifyItemRangeChanged(positionStart + WrapperAdapter.this.headers.size(), itemCount, payload);
        }

        @Override
        public void onItemRangeInserted(int positionStart, int itemCount) {
            WrapperAdapter.this.notifyItemRangeInserted(positionStart + WrapperAdapter.this.headers.size(), itemCount);
        }

        @Override
        public void onItemRangeRemoved(int positionStart, int itemCount) {
            WrapperAdapter.this.notifyItemRangeRemoved(positionStart + WrapperAdapter.this.headers.size(), itemCount);
        }

        @Override
        public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
            WrapperAdapter.this.notifyItemMoved(fromPosition + WrapperAdapter.this.headers.size(), toPosition + WrapperAdapter.this.headers.size());
        }
    }
}

Here are the main codes(Kotlin) of the recyclerView which uses custom adapter:

    private var wrapperAdapter: WrapperAdapter? = null
    override fun setAdapter(adapter: Adapter<*>?) {
        wrapperAdapter = if (adapter != null) {
            WrapperAdapter(adapter)
        } else {
            null
        }
        super.setAdapter(wrapperAdapter)
    }

    fun addHeader(header: View) {
        wrapperAdapter?.addHeader(header)
    }

    fun addFooter(footer: View) {
        wrapperAdapter?.addFooter(footer)
    }

    fun removeHeader(header: View) {
        wrapperAdapter?.removeHeader(header)
    }

    fun removeFooter(footer: View) {
        wrapperAdapter?.removeFooter(footer)
    }

The most useful answer of this question is worked for me. But I think it just avoid crashing instead of solving it to ensure this will cause no exceptions any more. So I don't think this is a good way.So I come to get help.

RKRK
  • 1,284
  • 5
  • 14
  • 18
EmMper
  • 125
  • 1
  • 8

1 Answers1

1

From the implementation of getItemCount(), it seems that the footers are only displayed when there are items. When the last item(s) are removed, you must notify the wrapper adapter that the footers are also removed.

Say there are 2 headers, 1 item, 3 footers : getItemCount() returns 6. If the inner item is removed, only 1 removal is propagated from the inner adapter, but getItemCount() now return 2 instead of the expected 5

I suggest to modify the AdapterDataObserverProxy :

@Override
public void onItemRangeRemoved(int positionStart, int itemCount) {
    WrapperAdapter.this.notifyItemRangeRemoved(positionStart + WrapperAdapter.this.headers.size(), itemCount);
    if (itemCount > 0 && innerAdapter.getItemCount() == 0 && footers.size() > 0) {
        // no more inner items, notify the removal of the footers
        int firstFooterPosition = headers.size();
        int footerCount = footers.size();
        WrapperAdapter.this.notifyItemRangeRemoved(firstFooterPosition, footerCount);
    }
}
bwt
  • 17,292
  • 1
  • 42
  • 60
  • I get you, and this will work. But in some situation, I want list could show its header even though the inner item is empty. So it is kind of unsuited. – EmMper Jun 27 '19 at 11:44
  • 1
    only the footers are reported as removed, the headers are still visible. – bwt Jun 27 '19 at 11:46
  • 1
    The point is : the changes notified must be consistent with the new state. When something is not displayed anymore (in this case the footers) you must notify the adapter – bwt Jun 27 '19 at 12:00
  • You are right, but I still have a question, if I have a recycler view which contains one header, one footer, and n(n > 0) items. I removed all items, so I called `wrapperAdapter.notifyItemRangeRemoved(headerSize, n);` to update views.The footer will not be updated and it shouldn't be notified at all, but actually it seems to be notified so that I have remove it. that is kind of unreasonable. – EmMper Jun 27 '19 at 12:32
  • 1
    I think the notifications regarding the items should be handled by the inner adapter, which will propagate them to the wrapper adapter via the observer. – bwt Jun 27 '19 at 15:26
  • You are right, and I know why the error caused. Thank you very much. – EmMper Jun 28 '19 at 03:07