airbnb/epoxy · error · IllegalStateException

No view stubs found. If viewgroup is not empty it must…

Error message

No view stubs found. If viewgroup is not empty it must contain ViewStubs.

What it means

ModelGroupHolder binds an EpoxyModelGroup by inflating a layout that must contain ViewStub entries, one per model. createViewStubData recursively collects those ViewStubs from the view group; if none are found, the ViewGroup passed to bind was empty of ViewStubs, so binding cannot proceed and the library throws this IllegalStateException. It exists to catch layout misuse early rather than fail later with confusing NPEs.

Solutions

  1. Use a layout resource whose direct/descendant children are ViewStub elements, one per model in the group.
  2. Verify the resource id passed when creating/inflating the group layout is the intended stub layout, not a regular item layout.
  3. Ensure the ViewGroup passed to bind is empty of other content and dedicated to the EpoxyModelGroup.
  4. If you migrated from regular views, convert each child view back to a <ViewStub> with the real layout in its layoutResource attribute.

Example fix

// before: res/layout/group_item.xml with real views
<LinearLayout ...>
  <TextView .../>
  <ImageView .../>
</LinearLayout>

// after: one ViewStub per model
<LinearLayout ...>
  <ViewStub android:id="@+id/stub1" android:layout="@layout/model1" .../>
  <ViewStub android:id="@+id/stub2" android:layout="@layout/model2" .../>
</LinearLayout>
Defensive patterns

Strategy: validation

Validate before calling

fun ViewGroup.validateHasViewStubs() {
  check(collectStubs(this).isNotEmpty()) {
    "Layout $id used for EpoxyModelGroup contains no ViewStubs"
  }
}

Prevention

When it happens

Trigger: Calling bind (or bindGroupIfNeeded indirectly) with a ViewGroup whose layout contains no ViewStub children at all — e.g. an empty container or a layout built from real views instead of ViewStubs.

Common situations: Passing a regular layout (LinearLayout/FrameLayout with normal views) where an EpoxyModelGroup stub layout is expected; accidentally inflating the wrong resource id; layouts refactored so ViewStubs were replaced with plain views; using a ViewGroup that is already populated with children.

Understand the failure class

Background: "is required", "must be set", "missing required field": configuration validation errors across open-source libraries — this error's family across 36 libraries.

Related errors


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

Appendix: source

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

    /**
     * By default the outermost viewgroup is used as the container that views are added to. However,
     * users can specify a different, nested view group to use as the child container by marking it
     * with a special id.
     */
    private fun findChildContainer(outermostRoot: ViewGroup): ViewGroup {
        val customRoot = outermostRoot.findViewById<View>(R.id.epoxy_model_group_child_container)

        return customRoot as? ViewGroup ?: outermostRoot
    }

    private fun createViewStubData(viewGroup: ViewGroup): List<ViewStubData> {
        return ArrayList<ViewStubData>(4).apply {

            collectViewStubs(viewGroup, this)

            if (isEmpty()) {
                throw IllegalStateException(
                    "No view stubs found. If viewgroup is not empty it must contain ViewStubs."
                )
            }
        }
    }

    private fun collectViewStubs(
        viewGroup: ViewGroup,
        stubs: ArrayList<ViewStubData>
    ) {
        for (i in 0 until viewGroup.childCount) {
            val child = viewGroup.getChildAt(i)

            if (child is ViewGroup) {
                collectViewStubs(child, stubs)
            } else if (child is ViewStub) {
                stubs.add(ViewStubData(viewGroup, child, i))
            }

View on GitHub (pinned to e45bd3a61f)