airbnb/epoxy · error · IllegalStateException

State cannot be restored once views have been bound. It…

Error message

State cannot be restored once views have been bound. It should be done before adding the adapter to the recycler view.

What it means

onRestoreInstanceState restores saved view-holder state, but applying it to already-bound views is not supported; Epoxy simplifies state restoration by only allowing it before any views are bound. If boundViewHolders is non-empty it throws instead of producing inconsistent UI state.

Solutions

  1. Restore state before adding the adapter to the RecyclerView (e.g. in onCreate before setContentView lays out, or immediately after creating the adapter).
  2. If state must be restored late, create a fresh adapter/RecyclerView, restore state on it before binding, then attach.
  3. Defer adapter attachment: set the adapter only after calling onRestoreInstanceState.

Example fix

// before
recyclerView.setController(controller);
controller.requestModelBuild();
recyclerView.restoreState(savedState); // throws: views already bound

// after
adapter.onRestoreInstanceState(savedState);
recyclerView.setAdapter(adapter); // restore before attaching/binding
Defensive patterns

Strategy: validation

Validate before calling

if (adapter.boundViewHolders == null || /* cannot access; instead */ !recyclerView.hasPendingAdapterUpdates() && recyclerView.getChildCount() == 0) { adapter.onRestoreInstanceState(bundle); }

Try / catch

try { adapter.onRestoreInstanceState(bundle); } catch (IllegalStateException e) { // fall back: adapter already bound; skip restore or recreate adapter }

Prevention

When it happens

Trigger: Calling adapter.onRestoreInstanceState(bundle) or recyclerView.restoreState(...) after the adapter has been attached to a RecyclerView and at least one view has been bound (boundViewHolders.size() > 0).

Common situations: Restoring state in onResume/onPostExecute after the RecyclerView already laid out, or calling restoreState on a second RecyclerView sharing the adapter that already scrolled/bound items.

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/0dbc307f65f4345d. Report an issue: GitHub.

Appendix: source

Thrown at epoxy-adapter/src/main/java/com/airbnb/epoxy/BaseEpoxyAdapter.java:242

    // 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(
            "Tried to restore instance state, but onSaveInstanceState was never called.");
      }
    }
  }

  /**
   * Finds the position of the given model in the list. Doesn't use indexOf to avoid unnecessary
   * equals() calls since we're looking for the same object instance.
   *
   * @return The position of the given model in the current models list, or -1 if the model can't be

View on GitHub (pinned to e45bd3a61f)