DrKLO/Telegram · critical · IllegalStateException

Two different ViewHolders have the same change ID. This migh

Error message

Two different ViewHolders have the same change ID. This might happen due to inconsistent Adapter update events or if the LayoutManager lays out the same View multiple times.
 ViewHolder 1:{other} 
 View Holder 2:{holder}

What it means

Same code path as error 42 but the else branch: two distinct attached ViewHolders share a change key while hasStableIds() is false. With stable ids off, the change key is position-derived, so a collision means either the LayoutManager laid out the same logical position twice, or the adapter emitted an inconsistent sequence of notify* events (e.g. notifyItemRangeChanged overlapping notifyItemRemoved) that confused AdapterHelper's position bookkeeping. The framework detects the ambiguity during Step 3 change matching and aborts.

Source

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

    private void handleMissingPreInfoForChangeError(long key,
            ViewHolder holder, ViewHolder oldChangeViewHolder) {
        // check if two VH have the same key, if so, print that as an error
        final int childCount = mChildHelper.getChildCount();
        for (int i = 0; i < childCount; i++) {
            View view = mChildHelper.getChildAt(i);
            ViewHolder other = getChildViewHolderInt(view);
            if (other == holder) {
                continue;
            }
            final long otherKey = getChangedHolderKey(other);
            if (otherKey == key) {
                if (mAdapter != null && mAdapter.hasStableIds()) {
                    throw new IllegalStateException("Two different ViewHolders have the same stable"
                            + " ID. Stable IDs in your adapter MUST BE unique and SHOULD NOT"
                            + " change.\n ViewHolder 1:" + other + " \n View Holder 2:" + holder
                            + exceptionLabel());
                } else {
                    throw new IllegalStateException("Two different ViewHolders have the same change"
                            + " ID. This might happen due to inconsistent Adapter update events or"
                            + " if the LayoutManager lays out the same View multiple times."
                            + "\n ViewHolder 1:" + other + " \n View Holder 2:" + holder
                            + exceptionLabel());
                }
            }
        }
        // Very unlikely to happen but if it does, notify the developer.
        Log.e(TAG, "Problem while matching changed view holders with the new"
                + "ones. The pre-layout information for the change holder " + oldChangeViewHolder
                + " cannot be found but it is necessary for " + holder + exceptionLabel());
    }

    /**
     * Records the animation information for a view holder that was bounced from hidden list. It
     * also clears the bounce back flag.
     */
    void recordAnimationInfoIfBouncedHiddenView(ViewHolder viewHolder,

View on GitHub (pinned to 45ab8f4308)

Solutions

  1. Verify every backing-list mutation has a matching, correctly-ranged notifyItemXxx call (or switch to DiffUtil/ListAdapter which computes correct events automatically).
  2. Ensure all adapter updates run on the main thread — background mutations race the layout pass and corrupt AdapterHelper state.
  3. If using a custom LayoutManager, confirm it does not add the same child/position twice in one layout pass; prefer using standard LinearLayoutManager/GridLayoutManager.
  4. Replace manual notify sequences with ListAdapter + DiffUtil to guarantee a consistent, minimal update event stream.

Example fix

// before: mutating list without matching notify, overlapping ranges
items.remove(index);
items.add(index, newItem);
notifyItemChanged(index); // count math wrong if size changed
// after: use ListAdapter + DiffUtil, or correct paired notifications
items.remove(index);
notifyItemRemoved(index);
items.add(index, newItem);
notifyItemInserted(index);
Defensive patterns

Strategy: validation

Validate before calling

// Before submitting an update, ensure each notify range matches the real delta
static void safeRemove(List<?> list, int idx, int count) {
  if (idx < 0 || idx + count > list.size()) {
    throw new IllegalArgumentException("notify range out of bounds");
  }
  list.subList(idx, idx + count).clear();
}

Prevention

When it happens

Trigger: A custom LayoutManager that adds the same view/position more than once in a single layout pass; calling notifyItemXxx events whose net effect contradicts each other (e.g. notifyItemInserted then notifyItemRangeRemoved covering overlapping ranges without a matching count change); off-by-one in notify range math so AdapterHelper's predicted positions drift; mutating the backing list without a corresponding notify call.

Common situations: Adapter backing list modified (add/remove) but no matching notifyItemXxx issued, then a layout pass runs and the position math collides; a diff algorithm producing conflicting move+change events; RecyclerView used with a data source updated from a non-main thread.

Related errors


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