DrKLO/Telegram · critical · IllegalArgumentException
only remove and update ops can be dispatched in first pass
Error message
only remove and update ops can be dispatched in first pass
What it means
dispatchFirstPassAndUpdateViewHolders runs the FIRST dispatch pass for ops that were processed against the pre-layout (invisible) view holders. Only REMOVE and UPDATE are valid here because those are the only op types that get routed into the pre-layout dispatch path. ADD and MOVE are always postponed to the second pass. Reaching the default branch means an op with an unexpected command was passed into the first-pass dispatch, which violates the contract established in dispatchAndUpdateViewHolders and is treated as an internal state corruption.
Source
Thrown at TMessagesProj/src/main/java/androidx/recyclerview/widget/AdapterHelper.java:358
Log.d(TAG, "postponed state after:");
for (UpdateOp updateOp : mPostponedList) {
Log.d(TAG, updateOp.toString());
}
Log.d(TAG, "----");
}
}
void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) {
mCallback.onDispatchFirstPass(op);
switch (op.cmd) {
case UpdateOp.REMOVE:
mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);
break;
case UpdateOp.UPDATE:
mCallback.markViewHoldersUpdated(offsetStart, op.itemCount, op.payload);
break;
default:
throw new IllegalArgumentException("only remove and update ops can be dispatched"
+ " in first pass");
}
}
private int updatePositionWithPostponed(int pos, int cmd) {
final int count = mPostponedList.size();
for (int i = count - 1; i >= 0; i--) {
UpdateOp postponed = mPostponedList.get(i);
if (postponed.cmd == UpdateOp.MOVE) {
int start, end;
if (postponed.positionStart < postponed.itemCount) {
start = postponed.positionStart;
end = postponed.itemCount;
} else {
start = postponed.itemCount;
end = postponed.positionStart;
}
if (pos >= start && pos <= end) {View on GitHub (pinned to 45ab8f4308)
Solutions
- Do not call dispatchFirstPassAndUpdateViewHolders from app code or custom layout managers; it is package-private internal API.
- Ensure the bundled AdapterHelper source and UpdateOp constants come from the same AndroidX release.
- Check ProGuard/R8 configuration is not aggressively optimizing the recyclerview widget package.
- Simplify update flow to DiffUtil.dispatchUpdatesTo so ops are always correctly typed.
Defensive patterns
Strategy: validation
Validate before calling
// Only dispatch REMOVE/UPDATE to first pass
boolean isFirstPassable(int cmd) {
return cmd == UpdateOp.REMOVE || cmd == UpdateOp.UPDATE;
} Prevention
- Treat dispatchFirstPassAndUpdateViewHolders as package-private; never call from app code.
- Ensure fork routing logic keeps ADD/MOVE on the second pass.
- Keep AdapterHelper and Callback source in sync across the fork.
When it happens
Trigger: A custom LayoutManager or ItemAnimator manually invokes consumeUpdatesInOneDeferredPass or dispatchFirstPassAndUpdateViewHolders with an op it constructed; an UpdateOp whose cmd changed between classification and dispatch; a fork that altered the routing in dispatchUpdatesInOneDeferredPass without updating this switch.
Common situations: Modifying the bundled RecyclerView source in a fork (common in Telegram-style apps) and introducing a new op type without updating all switch statements; an obfuscator that merged switch tables; reentrant notify calls during predictive item animation.
Related errors
- op should be remove or update.{}
- Unknown update op type for {}
- Providing a LayoutTransition into RecyclerView is not suppor
- Holder at{i} {holder} ...
- should not dispatch add or move for pre layout
AI-assisted analysis of DrKLO/Telegram@45ab8f4308 (2026-08-14).
Data as JSON: /api/errors/2d51b9ef351a381a.
Report an issue: GitHub.