DrKLO/Telegram · critical · IndexOutOfBoundsException

Inconsistency detected. Invalid view holder adapter position

Error message

Inconsistency detected. Invalid view holder adapter position{holder}

What it means

Recycler.validateViewHolderForOffsetPosition throws IndexOutOfBoundsException ('Inconsistency detected. Invalid view holder adapter position') when a cached/scrap ViewHolder's mPosition is outside [0, mAdapter.getItemCount()). This means the holder believes it represents an adapter position that no longer exists — the adapter's reported item count disagrees with the position the ViewHolder was bound to. It is the canonical symptom of an inconsistent adapter state: the backing data changed (in size or content) without a matching, correctly-sequenced set of notifyItemXxx calls, so the RecyclerView's position bookkeeping (via AdapterHelper) drifted out of sync with the adapter.

Source

Thrown at TMessagesProj/src/main/java/androidx/recyclerview/widget/RecyclerView.java:5992

         * Helper method for getViewForPosition.
         * <p>
         * Checks whether a given view holder can be used for the provided position.
         *
         * @param holder ViewHolder
         * @return true if ViewHolder matches the provided position, false otherwise
         */
        boolean validateViewHolderForOffsetPosition(ViewHolder holder) {
            // if it is a removed holder, nothing to verify since we cannot ask adapter anymore
            // if it is not removed, verify the type and id.
            if (holder.isRemoved()) {
                if (DEBUG && !mState.isPreLayout()) {
                    throw new IllegalStateException("should not receive a removed view unless it"
                            + " is pre layout" + exceptionLabel());
                }
                return mState.isPreLayout();
            }
            if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {
                throw new IndexOutOfBoundsException("Inconsistency detected. Invalid view holder "
                        + "adapter position" + holder + exceptionLabel());
            }
            if (!mState.isPreLayout()) {
                // don't check type if it is pre-layout.
                final int type = mAdapter.getItemViewType(holder.mPosition);
                if (type != holder.getItemViewType()) {
                    return false;
                }
            }
            if (mAdapter.hasStableIds()) {
                return holder.getItemId() == mAdapter.getItemId(holder.mPosition);
            }
            return true;
        }

        /**
         * Attempts to bind view, and account for relevant timing information. If
         * deadlineNs != FOREVER_NS, this method may fail to bind, and return false.

View on GitHub (pinned to 45ab8f4308)

Solutions

  1. Ensure every backing-list mutation is paired with the correct notify call (or switch to ListAdapter + a correct DiffUtil.ItemCallback) and runs on the main thread.
  2. If using a cursor or paged source, swap the data source atomically and call notifyItemRangeChanged appropriately, or use androidx.paging PagedList which handles this.
  3. Audit DiffUtil.ItemCallback.areItemsTheSame/areContentsTheSame for correctness (must be consistent with equals/hashCode of the id).
  4. As a stopgap when corruption is suspected, call adapter.notifyDataSetChanged() to force a full rebind (loses animations but re-synchronizes positions).

Example fix

// before: clearing list without notify, then partial repopulate
items.clear();
items.addAll(newItems);
notifyItemRangeInserted(0, newItems.size()); // size math wrong -> position drift
// after: atomic swap with full notify, or ListAdapter
List<T> copy = new ArrayList<>(items);
copy.clear();
copy.addAll(newItems);
items.clear(); items.addAll(newItems);
notifyDataSetChanged(); // or use ListAdapter.submitList(newItems)
Defensive patterns

Strategy: validation

Validate before calling

int count = adapter.getItemCount();
if (position < 0 || position >= count) {
  // stale position; do not request this holder — refresh adapter state first
}

Try / catch

try {
  adapter.notifyItemRangeRemoved(idx, n);
} catch (IndexOutOfBoundsException e) {
  // recover by full re-sync
  adapter.notifyDataSetChanged();
}

Prevention

When it happens

Trigger: Mutating the adapter's backing list without issuing notifyItemRemoved/Inserted for each size change; calling notifyDataSetChanged while recycled holders still reference old positions; concurrent background-thread mutation of the list; a DiffUtil whose areItemsTheSame/areContentsTheSame produce a move count that disagrees with the actual list delta; off-by-one in notifyItemRangeRemoved ranges.

Common situations: ListAdapter/DiffUtil with buggy equals; clearing a list then repopulating with notifyDataSetChanged mid-scroll; a Cursor-backed adapter whose cursor is swapped while a holder is being recycled; network refresh that mutates the list off the main thread then notifies inconsistently.

Related errors


AI-assisted analysis of DrKLO/Telegram@45ab8f4308 (2026-08-14). Data as JSON: /api/errors/dc1509c5ae3d17d3. Report an issue: GitHub.