DrKLO/Telegram · critical · IllegalStateException

Two different ViewHolders have the same stable ID. Stable ID

Error message

Two different ViewHolders have the same stable ID. Stable IDs in your adapter MUST BE unique and SHOULD NOT change.
 ViewHolder 1:{other} 
 View Holder 2:{holder}

What it means

handleMissingPreInfoForChangeError throws when two distinct attached ViewHolders resolve to the same change key AND the adapter reports hasStableIds() == true. The change key is derived from the stable id (getChangedHolderKey returns getItemId when stable ids are on). Two holders sharing a stable id is a hard contract violation: stable ids must be unique and stable for the lifetime of an item. The framework cannot reconcile change animations for colliding ids, so it aborts rather than silently animating the wrong view.

Source

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

     *
     * @param key The change key
     * @param holder Current ViewHolder
     * @param oldChangeViewHolder Changed ViewHolder
     */
    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());
    }

View on GitHub (pinned to 45ab8f4308)

Solutions

  1. Ensure getItemId returns a globally unique, immutable id per logical item — typically the item's database primary key or server id, never the adapter position.
  2. Deduplicate the data set before submitting it (filter by id) so no two simultaneous rows share an id.
  3. If items legitimately can duplicate, do NOT call setHasStableIds(true) — leave stable ids disabled and rely on position-based change notifications.
  4. Audit that getItemId does not derive its value from mutable fields (timestamp, counter) that can change between two notify passes.

Example fix

// before
adapter.setHasStableIds(true);
@Override public long getItemId(int position) {
  return items.get(position).createdAt.hashCode(); // not unique
}
// after
adapter.setHasStableIds(true);
@Override public long getItemId(int position) {
  return items.get(position).id; // unique, immutable primary key
}
Defensive patterns

Strategy: validation

Validate before calling

static void assertUniqueIds(List<Item> items) {
  Set<Long> seen = new HashSet<>();
  for (Item it : items) {
    if (!seen.add(it.id)) {
      throw new IllegalStateException("Duplicate stable id: " + it.id);
    }
  }
}

Prevention

When it happens

Trigger: Adapter.getItemId(position) returns the same value for two different live positions (e.g. hashing items that lack a natural id, or returning position instead of a stable id); data set contains duplicate id values; getItemId recomputed from mutable fields that change between binds; setHasStableIds(true) enabled without implementing getItemId (defaults to NO_ID/-1 for all).

Common situations: Returning adapter position from getItemId; using equals/hashCode-inconsistent objects whose id drifts; enabling stable ids on an adapter whose items genuinely can duplicate (same server id fetched twice); list updates where two distinct rows briefly carry the same id due to a partial server sync.

Related errors


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