{"record":{"id":"f54f1df63a5b12f0","repo":"airbnb/epoxy","slug":"epoxy-attribute-fields-on-a-model-cannot-be-changed-once-the","errorCode":null,"errorMessage":"Epoxy attribute fields on a model cannot be changed once the model is added to a controller. Check that these fields are not updated, or that the assigned objects are not mutated, outside of the buildModels method. The only exception is if the change is made inside an Interceptor callback. Consider using an interceptor if you need to change a model after it is added to the controller and before it is set on the adapter. If the model is already set on the adapter then you must call `requestModelBuild` instead to recreate all models.","messagePattern":"Epoxy attribute fields on a model cannot be changed once the model is added to a controller\\. Check that these fields are not updated, or that the assigned objects are not mutated, outside of the buildModels method\\. The only exception is if the change is made inside an Interceptor callback\\. Consider using an interceptor if you need to change a model after it is added to the controller and before it is set on the adapter\\. If the model is already set on the adapter then you must call `requestModelBuild` instead to recreate all models\\.","errorType":"exception","errorClass":"ImmutableModelException","httpStatus":null,"severity":"error","filePath":"epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModel.java","lineNumber":468,"sourceCode":"    return firstControllerAddedTo != null;\n  }\n\n  /**\n   * This is used internally by generated models to do validation checking when\n   * \"validateEpoxyModelUsage\" is enabled and the model is used with an {@link EpoxyController}.\n   * This method validates that it is ok to change this model. It is only valid if the model hasn't\n   * yet been added, or the change is being done from an {@link EpoxyController.Interceptor}\n   * callback.\n   * <p>\n   * This is also used to stage the model for implicitly adding it, if it is an AutoModel and\n   * implicit adding is enabled.\n   */\n  protected final void onMutation() {\n    // The model may be added to multiple controllers, in which case if it was already diffed\n    // and added to an adapter in one controller we don't want to even allow interceptors\n    // from changing the model in a different controller\n    if (isDebugValidationEnabled() && !currentlyInInterceptors) {\n      throw new ImmutableModelException(this,\n          getPosition(firstControllerAddedTo, this));\n    }\n\n    if (controllerToStageTo != null) {\n      controllerToStageTo.setStagedModel(this);\n    }\n  }\n\n  private static int getPosition(@NonNull EpoxyController controller,\n      @NonNull EpoxyModel<?> model) {\n    // If the model was added to multiple controllers, or was removed from the controller and then\n    // modified, this won't be correct. But those should be very rare cases that we don't need to\n    // worry about\n    if (controller.isBuildingModels()) {\n      return controller.getFirstIndexOfModelInBuildingList(model);\n    }\n\n    return controller.getAdapter().getModelPosition(model);","sourceCodeStart":450,"sourceCodeEnd":486,"githubUrl":"https://github.com/airbnb/epoxy/blob/e45bd3a61fe3a1f130e184f5b8dcf172ab99025a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModel.java#L450-L486","documentation":"When debug validation is enabled, Epoxy models are immutable once added to a controller: mutating attribute fields outside buildModels (or an Interceptor callback) would invalidate the hash snapshot used for diffing. onMutation detects such a change and throws ImmutableModelException with the model's position.","triggerScenarios":"Calling any mutating setter (layout(), reset(), show(), etc.) on a model after it was added to a controller while outside interceptors and with validation enabled.","commonSituations":"Updating a retained model in an onClick handler or after data refresh without rebuilding; mutating models inside interceptors incorrectly; enabling validateEpoxyModelUsage on a controller that mutates models after build.","solutions":["Rebuild the model inside buildModels with the new value instead of mutating it","Use controller.requestModelBuild() to trigger a rebuild after data changes","Perform the mutation inside an Interceptor callback, which is exempt from the check","Keep all model configuration at construction time inside buildModels"],"exampleFix":"// before\nmodel.show(false); // throws if model already added\n// after\nrequestModelBuild(); // in buildModels: new HeaderModel_().show(false)","handlingStrategy":"validation","validationCode":"if (model != null && addedModels.contains(model) && !inBuildModels) throw new IllegalStateException(\"Cannot mutate added model\");","typeGuard":null,"tryCatchPattern":"try { mutateModel(); } catch (ImmutableModelException e) { requestModelBuild(); }","preventionTips":["Mutate only inside buildModels","Use interceptors for pre-adapter changes","Use requestModelBuild() for post-adapter changes","Don't cache model instances across builds"],"tags":["android","epoxy","immutability","validation"],"backgroundTag":"invalid-state-transition","analyzedSha":"e45bd3a61fe3a1f130e184f5b8dcf172ab99025a","analyzedAt":"2026-09-13T03:24:16.052Z","contentChangedAt":"2026-09-13T03:24:16.052Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}