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
- Restore state before adding the adapter to the RecyclerView (e.g. in onCreate before setContentView lays out, or immediately after creating the adapter).
- If state must be restored late, create a fresh adapter/RecyclerView, restore state on it before binding, then attach.
- 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
- Restore state immediately after creating the adapter, before setAdapter.
- Create a fresh adapter instance when restoring late.
- Follow Epoxy's saveState/restoreState ordering on EpoxyRecyclerView.
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
- Tried to restore instance state, but onSaveInstanceState…
- Must have stable ids when saving view holder state
- numItemsToPrefetch must be greater than 0
- Unable to invoke
- Unable to get Epoxy helper class.
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 beView on GitHub (pinned to e45bd3a61f)