airbnb/epoxy · error · IllegalStateException

Group is not bound

Error message

Group is not bound

What it means

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.

Solutions

  1. Track bind state in your code and only call unbindGroup after a successful bind.
  2. Make unbind idempotent on your side by checking your own bound flag before delegating.
  3. If you control the calling code, remove the redundant unbind call (RecyclerView already calls unbind at the right time).
  4. Catch IllegalStateException around unbindGroup only if double-unbind is expected in your pool design.

Example fix

// before
holder.unbind()
holder.unbind() // second call throws

// after
if (holder.boundGroup != null) {
  holder.unbind()
}
Defensive patterns

Strategy: type-guard

Type guard

fun ModelGroupHolder.safeUnbind() {
  if (this.boundGroup != null) this.unbind()
}

Prevention

When it happens

Trigger: Calling unbindGroup() (or unbind()) twice in a row without an intervening bind; calling unbind on a holder that was never bound.

Common situations: Double-unbind from overlapping RecyclerView lifecycle callbacks (onViewRecycled plus onViewDetachedFromWindow); manual holder management where bind was skipped; custom pool/recycling code calling unbind defensively.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of airbnb/epoxy@e45bd3a61f (2026-09-13). Data as JSON: /api/errors/68e28c87691248ae. Report an issue: GitHub.

Appendix: source

Thrown at epoxy-adapter/src/main/java/com/airbnb/epoxy/ModelGroupHolder.kt:162

        return ViewTypeManager.getViewType(model1) == ViewTypeManager.getViewType(model2)
    }

    private fun getViewHolder(parent: ViewGroup, model: EpoxyModel<*>): EpoxyViewHolder {
        val viewType = ViewTypeManager.getViewType(model)
        val recycledView = viewPool.getRecycledView(viewType)

        return recycledView as? EpoxyViewHolder
            ?: HELPER_ADAPTER.createViewHolder(
                modelGroupParent,
                model,
                parent,
                viewType
            )
    }

    fun unbindGroup() {
        if (boundGroup == null) {
            throw IllegalStateException("Group is not bound")
        }

        repeat(viewHolders.size) {
            // Remove from the end for more efficient list actions
            removeAndRecycleView(viewHolders.size - 1)
        }

        boundGroup = null
    }

    private fun removeAndRecycleView(modelPosition: Int) {
        if (usingStubs()) {
            stubs[modelPosition].resetStub()
        } else {
            childContainer.removeViewAt(modelPosition)
        }

        val viewHolder = viewHolders.removeAt(modelPosition)

View on GitHub (pinned to e45bd3a61f)