{"record":{"id":"68e28c87691248ae","repo":"airbnb/epoxy","slug":"group-is-not-bound","errorCode":null,"errorMessage":"Group is not bound","messagePattern":"Group is not bound","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"epoxy-adapter/src/main/java/com/airbnb/epoxy/ModelGroupHolder.kt","lineNumber":162,"sourceCode":"        return ViewTypeManager.getViewType(model1) == ViewTypeManager.getViewType(model2)\n    }\n\n    private fun getViewHolder(parent: ViewGroup, model: EpoxyModel<*>): EpoxyViewHolder {\n        val viewType = ViewTypeManager.getViewType(model)\n        val recycledView = viewPool.getRecycledView(viewType)\n\n        return recycledView as? EpoxyViewHolder\n            ?: HELPER_ADAPTER.createViewHolder(\n                modelGroupParent,\n                model,\n                parent,\n                viewType\n            )\n    }\n\n    fun unbindGroup() {\n        if (boundGroup == null) {\n            throw IllegalStateException(\"Group is not bound\")\n        }\n\n        repeat(viewHolders.size) {\n            // Remove from the end for more efficient list actions\n            removeAndRecycleView(viewHolders.size - 1)\n        }\n\n        boundGroup = null\n    }\n\n    private fun removeAndRecycleView(modelPosition: Int) {\n        if (usingStubs()) {\n            stubs[modelPosition].resetStub()\n        } else {\n            childContainer.removeViewAt(modelPosition)\n        }\n\n        val viewHolder = viewHolders.removeAt(modelPosition)","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/airbnb/epoxy/blob/e45bd3a61fe3a1f130e184f5b8dcf172ab99025a/epoxy-adapter/src/main/java/com/airbnb/epoxy/ModelGroupHolder.kt#L144-L180","documentation":"unbindGroup releases the child ViewHolders created for the currently bound EpoxyModelGroup and clears boundGroup. If unbindGroup is called when no group is bound (boundGroup == null), there is nothing to release, and the holder throws this IllegalStateException to surface a lifecycle misuse.","triggerScenarios":"Calling unbindGroup() (or unbind()) twice in a row without an intervening bind; calling unbind on a holder that was never bound.","commonSituations":"Double-unbind from overlapping RecyclerView lifecycle callbacks (onViewRecycled plus onViewDetachedFromWindow); manual holder management where bind was skipped; custom pool/recycling code calling unbind defensively.","solutions":["Track bind state in your code and only call unbindGroup after a successful bind.","Make unbind idempotent on your side by checking your own bound flag before delegating.","If you control the calling code, remove the redundant unbind call (RecyclerView already calls unbind at the right time).","Catch IllegalStateException around unbindGroup only if double-unbind is expected in your pool design."],"exampleFix":"// before\nholder.unbind()\nholder.unbind() // second call throws\n\n// after\nif (holder.boundGroup != null) {\n  holder.unbind()\n}","handlingStrategy":"type-guard","validationCode":null,"typeGuard":"fun ModelGroupHolder.safeUnbind() {\n  if (this.boundGroup != null) this.unbind()\n}","tryCatchPattern":null,"preventionTips":["Let RecyclerView drive bind/unbind; avoid manual unbind calls.","Keep a boolean bound flag in wrapper code and make your unbind idempotent.","In custom pools, unbind only in onViewRecycled, not also in onDetachedFromWindow."],"tags":["android","epoxy","lifecycle","unbind","double-call"],"backgroundTag":"invalid-state-transition","analyzedSha":"e45bd3a61fe3a1f130e184f5b8dcf172ab99025a","analyzedAt":"2026-09-13T03:24:16.052Z","contentChangedAt":"2026-09-13T03:24:16.052Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}