{"record":{"id":"2d51b9ef351a381a","repo":"DrKLO/Telegram","slug":"only-remove-and-update-ops-can-be-dispatched-in-fi","errorCode":null,"errorMessage":"only remove and update ops can be dispatched in first pass","messagePattern":"only remove and update ops can be dispatched in first pass","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"critical","filePath":"TMessagesProj/src/main/java/androidx/recyclerview/widget/AdapterHelper.java","lineNumber":358,"sourceCode":"            Log.d(TAG, \"postponed state after:\");\n            for (UpdateOp updateOp : mPostponedList) {\n                Log.d(TAG, updateOp.toString());\n            }\n            Log.d(TAG, \"----\");\n        }\n    }\n\n    void dispatchFirstPassAndUpdateViewHolders(UpdateOp op, int offsetStart) {\n        mCallback.onDispatchFirstPass(op);\n        switch (op.cmd) {\n            case UpdateOp.REMOVE:\n                mCallback.offsetPositionsForRemovingInvisible(offsetStart, op.itemCount);\n                break;\n            case UpdateOp.UPDATE:\n                mCallback.markViewHoldersUpdated(offsetStart, op.itemCount, op.payload);\n                break;\n            default:\n                throw new IllegalArgumentException(\"only remove and update ops can be dispatched\"\n                        + \" in first pass\");\n        }\n    }\n\n    private int updatePositionWithPostponed(int pos, int cmd) {\n        final int count = mPostponedList.size();\n        for (int i = count - 1; i >= 0; i--) {\n            UpdateOp postponed = mPostponedList.get(i);\n            if (postponed.cmd == UpdateOp.MOVE) {\n                int start, end;\n                if (postponed.positionStart < postponed.itemCount) {\n                    start = postponed.positionStart;\n                    end = postponed.itemCount;\n                } else {\n                    start = postponed.itemCount;\n                    end = postponed.positionStart;\n                }\n                if (pos >= start && pos <= end) {","sourceCodeStart":340,"sourceCodeEnd":376,"githubUrl":"https://github.com/DrKLO/Telegram/blob/45ab8f4308496e1f01026a97fcdb0d58a5274474/TMessagesProj/src/main/java/androidx/recyclerview/widget/AdapterHelper.java#L340-L376","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Only dispatch REMOVE/UPDATE to first pass\nboolean isFirstPassable(int cmd) {\n    return cmd == UpdateOp.REMOVE || cmd == UpdateOp.UPDATE;\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["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."],"tags":["recyclerview","internal-invariant","first-pass","itemanimator"],"backgroundTag":null,"analyzedSha":"45ab8f4308496e1f01026a97fcdb0d58a5274474","analyzedAt":"2026-08-14T05:19:30.815Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}