airbnb/epoxy · error · IllegalStateException
Insufficient view stubs for EpoxyModelGroup. $modelCount…
Error message
Insufficient view stubs for EpoxyModelGroup. $modelCount models were provided but only ${stubs.size} view stubs exist. What it means
An EpoxyModelGroup needs at least one ViewStub per model so each model can be bound into its own stub. When the holder is in stub mode (usingStubs()) and the number of stubs in the layout is less than the number of models in the group, bindGroupIfNeeded throws this IllegalStateException because some models would have no view to bind to.
Solutions
- Add ViewStub entries to the group layout so stub count >= model count.
- Reduce the number of models in the EpoxyModelGroup to match the available stubs.
- Build the layout dynamically from the model list so stub count always tracks models.size().
- Check that you are not accidentally binding the same group layout to a larger model list after a refactor.
Example fix
// before EpoxyModelGroup(models = listOf(modelA, modelB, modelC), layoutRes = R.layout.group_two_stubs) // after: add a third ViewStub to group layout, or EpoxyModelGroup(models = listOf(modelA, modelB), layoutRes = R.layout.group_two_stubs)
Defensive patterns
Strategy: validation
Validate before calling
fun validateGroupLayout(modelCount: Int, stubLayoutRes: Int, context: Context) {
val stubs = countStubs(LayoutInflater.from(context).inflate(stubLayoutRes, null))
require(stubs >= modelCount) { "Need $modelCount stubs, layout has $stubs" }
} Prevention
- Generate the group layout from the model list or keep a 1:1 stub-per-model comment in the layout file.
- Add a unit test that binds every group layout with its maximum model count.
- Re-check stub counts whenever you change the size of a group's model list.
When it happens
Trigger: Creating an EpoxyModelGroup with N models while its layout contains fewer than N ViewStub children (e.g. 3 models, 2 stubs).
Common situations: Adding models to a group without updating the group's layout to add matching stubs; reusing one group layout for differently sized groups; dynamic model-count changes at runtime not mirrored in the layout.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- The layout provided to EpoxyModelGroup must be a ViewGroup
- No view stubs found. If viewgroup is not empty it must…
- No view exists at position $position
- Must have stable ids when saving view holder state
- State cannot be restored once views have been bound. It…
AI-assisted analysis of airbnb/epoxy@e45bd3a61f (2026-09-13).
Data as JSON: /api/errors/25d8857e9315dd71.
Report an issue: GitHub.
Appendix: source
Thrown at epoxy-adapter/src/main/java/com/airbnb/epoxy/ModelGroupHolder.kt:111
if (previouslyBoundGroup === group) {
return
} else if (previouslyBoundGroup != null) {
// A different group is being bound; this can happen when an onscreen model is changed.
// The models or their layouts could have changed, so views may need to be updated
if (previouslyBoundGroup.models.size > group.models.size) {
for (i in previouslyBoundGroup.models.size - 1 downTo group.models.size) {
removeAndRecycleView(i)
}
}
}
this.boundGroup = group
val models = group.models
val modelCount = models.size
if (usingStubs() && stubs.size < modelCount) {
throw IllegalStateException(
"Insufficient view stubs for EpoxyModelGroup. $modelCount models were provided but only ${stubs.size} view stubs exist."
)
}
viewHolders.ensureCapacity(modelCount)
for (i in 0 until modelCount) {
val model = models[i]
val previouslyBoundModel = previouslyBoundGroup?.models?.getOrNull(i)
val stubData = stubs.getOrNull(i)
val parent = stubData?.viewGroup ?: childContainer
if (previouslyBoundModel != null) {
if (areSameViewType(previouslyBoundModel, model)) {
continue
}
removeAndRecycleView(i)
}View on GitHub (pinned to e45bd3a61f)