{"record":{"id":"dc1509c5ae3d17d3","repo":"DrKLO/Telegram","slug":"inconsistency-detected-invalid-view-holder-adapte","errorCode":null,"errorMessage":"Inconsistency detected. Invalid view holder adapter position{holder}","messagePattern":"Inconsistency detected\\. Invalid view holder adapter position(.+?)","errorType":"exception","errorClass":"IndexOutOfBoundsException","httpStatus":null,"severity":"critical","filePath":"TMessagesProj/src/main/java/androidx/recyclerview/widget/RecyclerView.java","lineNumber":5992,"sourceCode":"         * Helper method for getViewForPosition.\n         * <p>\n         * Checks whether a given view holder can be used for the provided position.\n         *\n         * @param holder ViewHolder\n         * @return true if ViewHolder matches the provided position, false otherwise\n         */\n        boolean validateViewHolderForOffsetPosition(ViewHolder holder) {\n            // if it is a removed holder, nothing to verify since we cannot ask adapter anymore\n            // if it is not removed, verify the type and id.\n            if (holder.isRemoved()) {\n                if (DEBUG && !mState.isPreLayout()) {\n                    throw new IllegalStateException(\"should not receive a removed view unless it\"\n                            + \" is pre layout\" + exceptionLabel());\n                }\n                return mState.isPreLayout();\n            }\n            if (holder.mPosition < 0 || holder.mPosition >= mAdapter.getItemCount()) {\n                throw new IndexOutOfBoundsException(\"Inconsistency detected. Invalid view holder \"\n                        + \"adapter position\" + holder + exceptionLabel());\n            }\n            if (!mState.isPreLayout()) {\n                // don't check type if it is pre-layout.\n                final int type = mAdapter.getItemViewType(holder.mPosition);\n                if (type != holder.getItemViewType()) {\n                    return false;\n                }\n            }\n            if (mAdapter.hasStableIds()) {\n                return holder.getItemId() == mAdapter.getItemId(holder.mPosition);\n            }\n            return true;\n        }\n\n        /**\n         * Attempts to bind view, and account for relevant timing information. If\n         * deadlineNs != FOREVER_NS, this method may fail to bind, and return false.","sourceCodeStart":5974,"sourceCodeEnd":6010,"githubUrl":"https://github.com/DrKLO/Telegram/blob/45ab8f4308496e1f01026a97fcdb0d58a5274474/TMessagesProj/src/main/java/androidx/recyclerview/widget/RecyclerView.java#L5974-L6010","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before: clearing list without notify, then partial repopulate\nitems.clear();\nitems.addAll(newItems);\nnotifyItemRangeInserted(0, newItems.size()); // size math wrong -> position drift\n// after: atomic swap with full notify, or ListAdapter\nList<T> copy = new ArrayList<>(items);\ncopy.clear();\ncopy.addAll(newItems);\nitems.clear(); items.addAll(newItems);\nnotifyDataSetChanged(); // or use ListAdapter.submitList(newItems)","handlingStrategy":"validation","validationCode":"int count = adapter.getItemCount();\nif (position < 0 || position >= count) {\n  // stale position; do not request this holder — refresh adapter state first\n}","typeGuard":null,"tryCatchPattern":"try {\n  adapter.notifyItemRangeRemoved(idx, n);\n} catch (IndexOutOfBoundsException e) {\n  // recover by full re-sync\n  adapter.notifyDataSetChanged();\n}","preventionTips":["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()."],"tags":["recyclerview","adapter","notify","data-integrity","position-tracking"],"backgroundTag":null,"analyzedSha":"45ab8f4308496e1f01026a97fcdb0d58a5274474","analyzedAt":"2026-08-14T05:19:30.815Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}