DrKLO/Telegram · critical · IllegalArgumentException

should not dispatch add or move for pre layout

Error message

should not dispatch add or move for pre layout

What it means

Thrown inside dispatchAndUpdateViewHolders, which is the code path that processes update operations against the pre-layout (invisible) view holders. RecyclerView routes each pending UpdateOp either to dispatchAndUpdateViewHolders (for POSITION_TYPE_INVISIBLE ops) or to postponeAndUpdateViewHolders. ADD and MOVE operations are NEVER expected in the dispatch/pre-layout path because their semantics cannot be reconciled against postponed ops the way REMOVE and UPDATE can. Hitting this means the adapter fed an ADD or MOVE op into a state where it was classified as invisible-position pre-layout work, which is an internal invariant violation, not a normal API usage error.

Source

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

        }
        if (tmpCount != op.itemCount) { // all 1 effect
            Object payload = op.payload;
            recycleUpdateOp(op);
            op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, payload);
        }
        if (type == POSITION_TYPE_INVISIBLE) {
            dispatchAndUpdateViewHolders(op);
        } else {
            postponeAndUpdateViewHolders(op);
        }
    }

    private void dispatchAndUpdateViewHolders(UpdateOp op) {
        // tricky part.
        // traverse all postpones and revert their changes on this op if necessary, apply updated
        // dispatch to them since now they are after this op.
        if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {
            throw new IllegalArgumentException("should not dispatch add or move for pre layout");
        }
        if (DEBUG) {
            Log.d(TAG, "dispatch (pre)" + op);
            Log.d(TAG, "postponed state before:");
            for (UpdateOp updateOp : mPostponedList) {
                Log.d(TAG, updateOp.toString());
            }
            Log.d(TAG, "----");
        }

        // handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial
        // TODO Since move ops are pushed to end, we should not need this anymore
        int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);
        if (DEBUG) {
            Log.d(TAG, "pos:" + op.positionStart + ",updatedPos:" + tmpStart);
        }
        int tmpCnt = 1;
        int offsetPositionForPartial = op.positionStart;

View on GitHub (pinned to 45ab8f4308)

Solutions

  1. Stop calling notify* from within scroll/layout callbacks; post them to the next frame so they are not interleaved with an in-flight layout pass.
  2. Batch all structural changes via DiffUtil.calculateDiff(...).dispatchUpdatesTo(adapter) instead of issuing individual notifyItemMoved/notifyItemRangeInserted calls.
  3. Ensure notify calls run on the main thread and that no recursive notify happens while RecyclerView.isComputingLayout() is true.
  4. If using a custom LayoutManager, verify it does not call adapter methods or trigger layout re-entry from onLayoutChildren.

Example fix

// before
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override public void onScrolled(RecyclerView rv, int dx, int dy) {
        adapter.notifyItemMoved(from, to); // can race with layout
    }
});

// after
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
    @Override public void onScrolled(RecyclerView rv, int dx, int dy) {
        rv.post(() -> adapter.notifyItemMoved(from, to));
    }
});
Defensive patterns

Strategy: validation

Validate before calling

// Only notify when not computing layout; defer otherwise
if (recyclerView.isComputingLayout()) {
    recyclerView.post(() -> adapter.notifyItemMoved(from, to));
} else {
    adapter.notifyItemMoved(from, to);
}

Prevention

When it happens

Trigger: An adapter notifies RecyclerView of notifyItemRangeInserted or notifyItemMoved while a previous batch of updates is still being consumed during pre-layout, and the op gets tagged POSITION_TYPE_INVISIBLE. This typically requires nested/interleaved notify calls during an active layout pass (e.g. calling notify from inside a scroll listener or onLayoutChildren). A custom LayoutManager that incorrectly reports positions for disappearing views can also cause misclassification.

Common situations: Calling adapter notify methods from a background thread that races with layout; chaining notifyItemInserted immediately after notifyItemRangeRemoved inside the same frame without using DiffUtil; a custom RecyclerView subclass or ItemAnimator that re-enters the adapter during animation; AndroidX version skew where the bundled AdapterHelper differs from the LayoutManager expectations.

Related errors


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