airbnb/epoxy · error · IllegalStateException
Must have stable ids when saving view holder state
Error message
Must have stable ids when saving view holder state
What it means
BaseEpoxyAdapter saves view-holder state (scroll positions, subview state) in onSaveInstanceState, but restoring that state later requires matching each saved item by adapter item id. If the adapter does not have stable ids enabled, saved state cannot be mapped back to models, so the library throws to prevent silently losing state.
Solutions
- Ensure all models have unique, stable ids (use @AutoModel, id(...), or distinct hash-based ids).
- Enable stable ids on the adapter: adapter.setHasStableIds(true) or build models with stable ids so Epoxy sets it.
- If stable ids are not possible, do not enable view-holder state saving for that adapter.
- Call onSaveInstanceState only when you know stable ids are enabled.
Example fix
// before EpoxyRecyclerView recyclerView = ...; // models without explicit ids, state saving enabled Bundle state = recyclerView.saveState(); // after adapter.setHasStableIds(true); // or use @AutoModel / model.id(stableId) Bundle state = recyclerView.saveState();
Defensive patterns
Strategy: validation
Validate before calling
if (adapter.hasStableIds()) { Bundle out = new Bundle(); adapter.onSaveInstanceState(out); } else { /* skip state saving or assign stable ids first */ } Try / catch
try { adapter.onSaveInstanceState(bundle); } catch (IllegalStateException e) { Log.w("Epoxy", "state save skipped: " + e.getMessage()); } Prevention
- Always assign explicit stable ids to models (model.id(...), @AutoModel).
- Enable setHasStableIds(true) whenever you plan to save view state.
- Test state save/restore across configuration changes early.
When it happens
Trigger: Calling epoxyRecyclerView.saveState() / adapter.onSaveInstanceState(bundle) when hasStableIds() is false and at least one view holder is bound with saved state (viewHolderState.size() > 0).
Common situations: Enabling state saving (e.g. withDisabledParenStateSaving off, carousel state saving) on an EpoxyRecyclerView whose models lack unique ids or whose adapter was built without setting hasStableIds(true). Common with models using auto-generated or duplicate ids.
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
- You must have stable ids to use diffing
- State cannot be restored once views have been bound. It…
- Tried to restore instance state, but onSaveInstanceState…
- numItemsToPrefetch must be greater than 0
- Unable to invoke
AI-assisted analysis of airbnb/epoxy@e45bd3a61f (2026-09-13).
Data as JSON: /api/errors/e000419be2c1e4d1.
Report an issue: GitHub.
Appendix: source
Thrown at epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java:232
}
@CallSuper
@Override
public void onViewDetachedFromWindow(EpoxyViewHolder holder) {
//noinspection unchecked,rawtypes
((EpoxyModel) holder.getModel()).onViewDetachedFromWindow(holder.objectToBind());
}
public void onSaveInstanceState(Bundle outState) {
// Save the state of currently bound views first so they are included. Views that were
// scrolled off and unbound will already have had
// their state saved.
for (EpoxyViewHolder holder : boundViewHolders) {
viewHolderState.save(holder);
}
if (viewHolderState.size() > 0 && !hasStableIds()) {
throw new IllegalStateException("Must have stable ids when saving view holder state");
}
outState.putParcelable(SAVED_STATE_ARG_VIEW_HOLDERS, viewHolderState);
}
public void onRestoreInstanceState(@Nullable Bundle inState) {
// To simplify things we enforce that state is restored before views are bound, otherwise it
// is more difficult to update view state once they are bound
if (boundViewHolders.size() > 0) {
throw new IllegalStateException(
"State cannot be restored once views have been bound. It should be done before adding "
+ "the adapter to the recycler view.");
}
if (inState != null) {
viewHolderState = inState.getParcelable(SAVED_STATE_ARG_VIEW_HOLDERS);
if (viewHolderState == null) {
throw new IllegalStateException(View on GitHub (pinned to e45bd3a61f)