{"record":{"id":"0143832e37a2d7db","repo":"DrKLO/Telegram","slug":"should-not-dispatch-add-or-move-for-pre-layout","errorCode":null,"errorMessage":"should not dispatch add or move for pre layout","messagePattern":"should not dispatch add or move for pre layout","errorType":"exception","errorClass":"IllegalArgumentException","httpStatus":null,"severity":"critical","filePath":"TMessagesProj/src/main/java/androidx/recyclerview/widget/AdapterHelper.java","lineNumber":266,"sourceCode":"        }\n        if (tmpCount != op.itemCount) { // all 1 effect\n            Object payload = op.payload;\n            recycleUpdateOp(op);\n            op = obtainUpdateOp(UpdateOp.UPDATE, tmpStart, tmpCount, payload);\n        }\n        if (type == POSITION_TYPE_INVISIBLE) {\n            dispatchAndUpdateViewHolders(op);\n        } else {\n            postponeAndUpdateViewHolders(op);\n        }\n    }\n\n    private void dispatchAndUpdateViewHolders(UpdateOp op) {\n        // tricky part.\n        // traverse all postpones and revert their changes on this op if necessary, apply updated\n        // dispatch to them since now they are after this op.\n        if (op.cmd == UpdateOp.ADD || op.cmd == UpdateOp.MOVE) {\n            throw new IllegalArgumentException(\"should not dispatch add or move for pre layout\");\n        }\n        if (DEBUG) {\n            Log.d(TAG, \"dispatch (pre)\" + op);\n            Log.d(TAG, \"postponed state before:\");\n            for (UpdateOp updateOp : mPostponedList) {\n                Log.d(TAG, updateOp.toString());\n            }\n            Log.d(TAG, \"----\");\n        }\n\n        // handle each pos 1 by 1 to ensure continuity. If it breaks, dispatch partial\n        // TODO Since move ops are pushed to end, we should not need this anymore\n        int tmpStart = updatePositionWithPostponed(op.positionStart, op.cmd);\n        if (DEBUG) {\n            Log.d(TAG, \"pos:\" + op.positionStart + \",updatedPos:\" + tmpStart);\n        }\n        int tmpCnt = 1;\n        int offsetPositionForPartial = op.positionStart;","sourceCodeStart":248,"sourceCodeEnd":284,"githubUrl":"https://github.com/DrKLO/Telegram/blob/45ab8f4308496e1f01026a97fcdb0d58a5274474/TMessagesProj/src/main/java/androidx/recyclerview/widget/AdapterHelper.java#L248-L284","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["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.","Batch all structural changes via DiffUtil.calculateDiff(...).dispatchUpdatesTo(adapter) instead of issuing individual notifyItemMoved/notifyItemRangeInserted calls.","Ensure notify calls run on the main thread and that no recursive notify happens while RecyclerView.isComputingLayout() is true.","If using a custom LayoutManager, verify it does not call adapter methods or trigger layout re-entry from onLayoutChildren."],"exampleFix":"// before\nrecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {\n    @Override public void onScrolled(RecyclerView rv, int dx, int dy) {\n        adapter.notifyItemMoved(from, to); // can race with layout\n    }\n});\n\n// after\nrecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {\n    @Override public void onScrolled(RecyclerView rv, int dx, int dy) {\n        rv.post(() -> adapter.notifyItemMoved(from, to));\n    }\n});","handlingStrategy":"validation","validationCode":"// Only notify when not computing layout; defer otherwise\nif (recyclerView.isComputingLayout()) {\n    recyclerView.post(() -> adapter.notifyItemMoved(from, to));\n} else {\n    adapter.notifyItemMoved(from, to);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never call adapter.notify* from inside onScroll/onLayout callbacks; post to the next frame.","Use DiffUtil.dispatchUpdatesTo instead of issuing interleaved notify calls.","Keep all notify calls on the main thread and outside isComputingLayout() windows."],"tags":["recyclerview","layout","adapter","threading","pre-layout"],"backgroundTag":null,"analyzedSha":"45ab8f4308496e1f01026a97fcdb0d58a5274474","analyzedAt":"2026-08-14T05:19:30.815Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}