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

  1. Ensure all models have unique, stable ids (use @AutoModel, id(...), or distinct hash-based ids).
  2. Enable stable ids on the adapter: adapter.setHasStableIds(true) or build models with stable ids so Epoxy sets it.
  3. If stable ids are not possible, do not enable view-holder state saving for that adapter.
  4. 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

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


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)