I am using TypedEpoxyController and i want to update item of my gridview. I have a model with epoxy attribute in it. When i changing data in this attribute object and calling setData on TypedEpoxyController i am getting such exception:
comcom.airbnb.epoxy.ImmutableModelException: Model was changed before it could be diffed 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.
As i see setData of TypedEpoxyController calls requestModelBuild and prevents from calling it directly. So why i am getting this exception and how i can update cell in gridview using epoxy?
can you post the code of your model and your controller?
That error is happening because the state of the model changed after it was added to the controller.
Model:
@EpoxyModelClass public class PhotoItemModel extends EpoxyModelWithHolder{ @EpoxyAttribute Photo photo; @Override public void bind(PhotoViewHolder holder) { super.bind(holder); holder.mSelectablePhotoView.setSelected(photo.isSelected()); holder.mSelectablePhotoView.setOnClickListener(v -> { photo.setSelected(!photo.isSelected); // here i am changing attribute EvenBus.getDefault.post(new PhotoEvent.PhotoClicked(); //after catching this event setData called }); ...
Controller:
public class PhotoController extends TypedEpoxyController> { @Override public void buildModels( List
data) { if (data == null || data.isEmpty()) { return; } for (Photo photo : data) { new PhotoItemModel_() .id(photo.getPhotoId()) .photo(photo)) .addTo(this); } } }
Basically what i want to achieve - is adapter's notifyItemChanged to be called by calling setData(with requestModelBuild) but its throwing error. How can item be updated without model change?
Similiar problem https://github.com/airbnb/epoxy/issues/201 . Proposed solution about isSelected field inside model could work but later i need to iterate thru List of Photo objects and detect which of them are selected
you're right the problem is photo.setSelected(!photo.isSelected);. like the error says, you can't change data in the model. that data must be treated as immutable.
How you structure that is up to you, but I can offer a few suggestions:
You could set up the model to have an extra attribute for whether it is selected
@EpoxyModelClass
public class PhotoItemModel extends EpoxyModelWithHolder {
@EpoxyAttribute Photo photo;
@EpoxyAttribute boolean isSelected
@Override
public void bind(PhotoViewHolder holder) {
super.bind(holder);
holder.mSelectablePhotoView.setSelected(isSelected);
holder.mSelectablePhotoView.setOnClickListener(v -> {
EvenBus.getDefault.post(new PhotoEvent.PhotoClicked();
});
Then change the controller to pass whether each item is selected. You will have to change your data structure to track that.
@Override
public void buildModels(List data) {
for (Photo photo : data) {
new PhotoItemModel_()
.id(photo.getPhotoId())
.photo(photo))
.isSelected(// todo)
.addTo(this);
}
}
Using kotlin data classes makes this easy
data class PhotoData(val isSelected: Boolean = false, val photo: Photo)
Alternatively if you want to keep the isSelected field inside the Photo class you can do that, but in your bus callback you will need to create a _new_ photo object and update your data list with it. You cannot update the existing photo object. They must be immutable. Using Kotlin data classes makes that trivial and forces you to do it correctly.
Personally I don't think it makes sense to couple the view state of whether something is selected inside the data model representing it. It is simpler, but not a good pattern.
@elihart I'm having a similar issue. I use Glide to load an image into my model but as it finishes in another thread when it tries to set the image it's been added to the controller and therefore throws this error. I'm using data binding and Kotlin.
Any ideas?
recyclerView.withModels {
andis.forEach {
andiListItem {
id("Andi" + it.andiId)
andi(it)
Glide
.with(this@MainActivity)
.asBitmap()
.load(it.imageUrl)
.into(object : SimpleTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap?, <---- throws here
transition: Transition<in Bitmap>?) {
image(resource)
}
})
}
divider {
id("Divider" + it.andiId)
}
}
}
com.airbnb.epoxy.ImmutableModelException: Position: 22 Model: AndiListItemBindingModel_{andi=com.jlaird.anddeployment.db.entities.Andi@cdc687e, image=null}AndiListItemBindingModel_{id=8093814774180635874, viewType=2131361821, shown=true, addedToAdapter=true}
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.
at com.airbnb.epoxy.EpoxyModel.onMutation(EpoxyModel.java:427)
at com.jlaird.anddeployment.AndiListItemBindingModel_.image(AndiListItemBindingModel_.java:98)
at com.jlaird.anddeployment.AndiListItemBindingModel_.image(AndiListItemBindingModel_.java:24)
at com.jlaird.anddeployment.main.MainActivity$displayAndisOnRecyclerView$1$1$1$1.onResourceReady(MainActivity.kt:64)
at com.jlaird.anddeployment.main.MainActivity$displayAndisOnRecyclerView$1$1$1$1.onResourceReady(MainActivity.kt:61)
at com.bumptech.glide.request.SingleRequest.onResourceReady(SingleRequest.java:560)
at com.bumptech.glide.request.SingleRequest.onResourceReady(SingleRequest.java:530)
at com.bumptech.glide.load.engine.EngineJob.handleResultOnMainThread(EngineJob.java:207)
at com.bumptech.glide.load.engine.EngineJob$MainThreadCallback.handleMessage(EngineJob.java:311)
at android.os.Handler.dispatchMessage(Handler.java:101)
at android.os.Looper.loop(Looper.java:164)
at android.app.ActivityThread.main(ActivityThread.java:6541)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.Zygote$MethodAndArgsCaller.run(Zygote.java:240)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:767)
I'm guessing image is some field on your model?
This doesn't work - you can't change the model after it is created. Another issue here is that you are starting the glide load for every single model in the list, that is hugely inefficient; you only want glide to start loading once the item is on screen.
what you should do instead is pass the image url to the model, and load it from there. That way the image is only loaded once the view is bound on screen.
We use glide as well, and set up a simple custom view that extends ImageView, takes a url, and loads the image. You can also use binding adapters (https://github.com/bumptech/glide/issues/932)
I also recommend clearing the imageview when the view is recycled.
On a separate note, you shouldn't have to concatenate your ids like id("Andi" + it.andiId). there are several id overloads like id(String, Long) to avoid creating a new string
Most helpful comment
I'm guessing
imageis some field on your model?This doesn't work - you can't change the model after it is created. Another issue here is that you are starting the glide load for every single model in the list, that is hugely inefficient; you only want glide to start loading once the item is on screen.
what you should do instead is pass the image url to the model, and load it from there. That way the image is only loaded once the view is bound on screen.
We use glide as well, and set up a simple custom view that extends ImageView, takes a url, and loads the image. You can also use binding adapters (https://github.com/bumptech/glide/issues/932)
I also recommend clearing the imageview when the view is recycled.
On a separate note, you shouldn't have to concatenate your ids like
id("Andi" + it.andiId). there are several id overloads likeid(String, Long)to avoid creating a new string