{"record":{"id":"a856d26bddb68830","repo":"airbnb/epoxy","slug":"a-controller-must-be-provided-in-the-constructor-if-dragging","errorCode":null,"errorMessage":"A controller must be provided in the constructor if dragging is enabled","messagePattern":"A controller must be provided in the constructor if dragging is enabled","errorType":"exception","errorClass":"IllegalStateException","httpStatus":null,"severity":"error","filePath":"epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModelTouchCallback.java","lineNumber":71,"sourceCode":"  }\n\n  @Override\n  protected boolean canDropOver(RecyclerView recyclerView, EpoxyViewHolder current,\n      EpoxyViewHolder target) {\n    // By default we don't allow dropping on a model that isn't a drag target\n    return isTouchableModel(target.getModel());\n  }\n\n  protected boolean isTouchableModel(EpoxyModel<?> model) {\n    return targetModelClass.isInstance(model);\n  }\n\n  @Override\n  protected boolean onMove(RecyclerView recyclerView, EpoxyViewHolder viewHolder,\n      EpoxyViewHolder target) {\n\n    if (controller == null) {\n      throw new IllegalStateException(\n          \"A controller must be provided in the constructor if dragging is enabled\");\n    }\n\n    int fromPosition = viewHolder.getAdapterPosition();\n    int toPosition = target.getAdapterPosition();\n    controller.moveModel(fromPosition, toPosition);\n\n    EpoxyModel<?> model = viewHolder.getModel();\n    if (!isTouchableModel(model)) {\n      throw new IllegalStateException(\n          \"A model was dragged that is not a valid target: \" + model.getClass());\n    }\n\n    //noinspection unchecked\n    onModelMoved(fromPosition, toPosition, (T) model, viewHolder.itemView);\n    return true;\n  }\n","sourceCodeStart":53,"sourceCodeEnd":89,"githubUrl":"https://github.com/airbnb/epoxy/blob/e45bd3a61fe3a1f130e184f5b8dcf172ab99025a/epoxy-adapter/src/main/java/com/airbnb/epoxy/EpoxyModelTouchCallback.java#L53-L89","documentation":"EpoxyModelTouchCallback moves models by delegating to controller.moveModel(from, to). If dragging is enabled but no controller was supplied to the callback's constructor, a drag's onMove cannot be routed, so Epoxy throws IllegalStateException before performing the move.","triggerScenarios":"Attaching an ItemTouchHelper with an EpoxyModelTouchCallback subclass whose constructor was given a null controller (or whose isDragEnabled returns true) and then dragging an item, triggering onMove.","commonSituations":"Instantiating the touch callback without passing the EpoxyController; the controller not yet created when the callback is constructed; copy-pasting touch callback setup that omits the controller argument.","solutions":["Pass the controller into the EpoxyModelTouchCallback constructor","Ensure the controller is initialized before attaching the ItemTouchHelper","Only enable dragging (isDragEnabled) when a controller is available"],"exampleFix":"// before\nnew MyTouchCallback(null); // drag throws in onMove\n// after\nnew MyTouchCallback(myEpoxyController);","handlingStrategy":"type-guard","validationCode":"if (touchCallback.getController() == null) { throw new IllegalStateException(\"Attach touch helper only after controller is set\"); }","typeGuard":"if (callback == null || callback.getController() == null) return; // skip attaching ItemTouchHelper","tryCatchPattern":"try { itemTouchHelper.attachToRecyclerView(rv); } catch (IllegalStateException e) { Log.e(TAG, \"Drag without controller\", e); }","preventionTips":["Pass the controller into the touch callback constructor","Attach ItemTouchHelper only after controller initialization","Enable dragging only when a controller exists"],"tags":["android","epoxy","drag-and-drop","missing-dependency"],"backgroundTag":"missing-required-argument","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"}