DrKLO/Telegram · error · IllegalArgumentException

Moving more than 1 item is not supported yet

Error message

Moving more than 1 item is not supported yet

What it means

AdapterHelper.onItemRangeMoved is the entry point for adapter.notifyItemMoved(from, to). RecyclerView's move model is strictly single-item: a move always relocates exactly one element. There is no notifyItemRangeMoved API that moves a contiguous block, so itemCount must be 1. Passing itemCount != 1 is invalid and unsupported, so the helper refuses it rather than silently producing a wrong layout. Most commonly this is hit by custom adapter code that calls the internal preUpdate or onItemRangeMoved directly with a range.

Source

Thrown at TMessagesProj/src/main/java/androidx/recyclerview/widget/AdapterHelper.java:587

            return false;
        }
        if (BuildVars.DEBUG_VERSION) {
            logNotify("onItemRangeRemoved(" + positionStart + ", " + itemCount + ")");
        }
        mPendingUpdates.add(obtainUpdateOp(UpdateOp.REMOVE, positionStart, itemCount, null));
        mExistingUpdateTypes |= UpdateOp.REMOVE;
        return mPendingUpdates.size() == 1;
    }

    /**
     * @return True if updates should be processed.
     */
    boolean onItemRangeMoved(int from, int to, int itemCount) {
        if (from == to) {
            return false; // no-op
        }
        if (itemCount != 1) {
            throw new IllegalArgumentException("Moving more than 1 item is not supported yet");
        }
        if (BuildVars.DEBUG_VERSION) {
            logNotify("onItemRangeMoved(" + from + ", " + to + ", " + itemCount + ")");
        }
        mPendingUpdates.add(obtainUpdateOp(UpdateOp.MOVE, from, to, null));
        mExistingUpdateTypes |= UpdateOp.MOVE;
        return mPendingUpdates.size() == 1;
    }

    /**
     * Skips pre-processing and applies all updates in one pass.
     */
    void consumeUpdatesInOnePass() {
        // we still consume postponed updates (if there is) in case there was a pre-process call
        // w/o a matching consumePostponedUpdates.
        consumePostponedUpdates();
        final int count = mPendingUpdates.size();
        for (int i = 0; i < count; i++) {

View on GitHub (pinned to 45ab8f4308)

Solutions

  1. Replace a multi-item move with one notifyItemMoved per item, or better, express it as remove + insert and let DiffUtil batch it.
  2. If implementing drag-to-reorder, call notifyItemMoved once per swapped adjacent pair.
  3. Never call onItemRangeMoved directly from app code; use the public RecyclerView.Adapter.notifyItemMoved(int,int).

Example fix

// before
// attempting to move 3 items at once
adapterHelper.onItemRangeMoved(from, to, 3); // throws

// after
for (int i = 0; i < 3; i++) {
    adapter.notifyItemMoved(from + i, to + i);
}
Defensive patterns

Strategy: validation

Validate before calling

// Enforce single-item move contract before notifying
void safeMove(RecyclerView.Adapter<?> a, int from, int to, int count) {
    if (count != 1) throw new IllegalArgumentException("move count must be 1, was " + count);
    a.notifyItemMoved(from, to);
}

Prevention

When it happens

Trigger: A custom adapter that calls adapterHelper.onItemRangeMoved(from, to, n) with n>1; wrapping notifyItemMoved inside a loop without DiffUtil; a data model where a 'move' is actually a bulk reordering incorrectly mapped onto a single move op.

Common situations: Bulk drag-and-drop reorder where the developer assumes a multi-item move primitive exists; porting code from a list library that supports range moves; a DiffUtil-like custom diff that emits multi-item moves instead of remove+insert pairs.

Related errors


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