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
- 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.
- If using a cursor or paged source, swap the data source atomically and call notifyItemRangeChanged appropriately, or use androidx.paging PagedList which handles this.
- Audit DiffUtil.ItemCallback.areItemsTheSame/areContentsTheSame for correctness (must be consistent with equals/hashCode of the id).
- 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
- Keep list mutation and notify calls atomic and on the main thread.
- Use ListAdapter + a correct DiffUtil.ItemCallback instead of manual notify ranges.
- Never cache adapter positions across data changes without re-validating against getItemCount().
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
- Inconsistency detected. Invalid item position {position}(off
- Two different ViewHolders have the same change ID. This migh
- Two different ViewHolders have the same stable ID. Stable ID
- should not dispatch add or move for pre layout
- Moving more than 1 item is not supported yet
AI-assisted analysis of DrKLO/Telegram@45ab8f4308 (2026-08-14).
Data as JSON: /api/errors/dc1509c5ae3d17d3.
Report an issue: GitHub.