airbnb / epoxy

Epoxy is an Android library for building complex screens in a RecyclerView
https://goo.gl/eIK82p
Apache License 2.0
8.5k stars 733 forks source link

[Kotlin] Generated Epoxy Models not working anymore #513

Closed pwillmann closed 6 years ago

pwillmann commented 6 years ago

After updating from 2.14.0 to 2.15.1 or anything newer (tried up to 2.16.2) using the generated Models directly instead of their Kotlin extension function will throw an Cannot create an instance of an abstract class exception.

This is especially frustrating for carousels since the provided withModelsFrom(...) extension function expects you to use the generatedWhateverViewModel_().id(...)... instead of the extension function which returns Unit.

Example EpoxyModel:

@EpoxyModelClass(layout = R2.layout.ui_recyclerview_epoxy_shift)
abstract class ShiftEpoxyModel : EpoxyModelWithHolder<ShiftEpoxyModel.ShiftViewHolder>() {
    @EpoxyAttribute
    var day: String = ""

    @EpoxyAttribute
    @StringRes
    var dayRes: Int? = null

    @EpoxyAttribute
    var beginDate: Date = Date()

    @EpoxyAttribute
    var endDate: Date = Date()

    override fun bind(holder: ShiftViewHolder) {
        super.bind(holder)
        ...
    }

    class ShiftViewHolder : EpoxyHolder() {
        lateinit var dayTextView: TextView
        lateinit var dateTextView: TextView
        lateinit var timeTextView: TextView

        override fun bindView(itemView: View) {
            dayTextView = itemView.findViewById(R.id.day)
            dateTextView = itemView.findViewById(R.id.date)
            timeTextView = itemView.findViewById(R.id.time)
        }
    }
}

In the controller

 shift {
                id("123")
                beginDate(beginDate)
                endDate(endDate)
}

works just fine,

 add(ShiftEpoxyModel_()
                    .id("123")
                    .beginDate(beginDate)
                    .endDate(endDate))

throws an Cannot create an instance of an abstract class exception.

Same thing when trying to create a carousel like this:

carousel {
            id("shifts-carousel")
            withModelsFrom(data.offer!!.shifts) {
                ShiftEpoxyModel_()
                        .id(it.toString())
                        .beginDate(it.beginDate)
                        .endDate(it.endDate)
            }
        }

Am i missing something ?

ShaishavGandhi commented 6 years ago

That seems weird. The extension under the hood calls the same ShiftEpoxyModel_() so if it were to fail, it would fail for both.

I tried your sample model and it builds fine. It would help if you can paste the generated ShiftEpoxyModel_ class.

pwillmann commented 6 years ago

Okay so I cleaned and rebuild everything again. As before with 2.14.0 everything works as expected, 2.16.2 produces Cannot create an instance of an abstract class. Nothing else was changed.

With Version 2.14.0 the generated class looks like this:


/**
 * Generated file. Do not modify! */
public class ShiftEpoxyModel_ extends ShiftEpoxyModel implements GeneratedModel<ShiftEpoxyModel.ShiftViewHolder>, ShiftEpoxyModelBuilder {
  private OnModelBoundListener<ShiftEpoxyModel_, ShiftEpoxyModel.ShiftViewHolder> onModelBoundListener_epoxyGeneratedModel;

  private OnModelUnboundListener<ShiftEpoxyModel_, ShiftEpoxyModel.ShiftViewHolder> onModelUnboundListener_epoxyGeneratedModel;

  public ShiftEpoxyModel_() {
    super();
  }

  @Override
  public void addTo(EpoxyController controller) {
    super.addTo(controller);
    addWithDebugValidation(controller);
  }

  @Override
  public void handlePreBind(final EpoxyViewHolder holder,
      final ShiftEpoxyModel.ShiftViewHolder object, final int position) {
    validateStateHasNotChangedSinceAdded("The model was changed between being added to the controller and being bound.", position);
  }

  @Override
  public void handlePostBind(final ShiftEpoxyModel.ShiftViewHolder object, int position) {
    if (onModelBoundListener_epoxyGeneratedModel != null) {
      onModelBoundListener_epoxyGeneratedModel.onModelBound(this, object, position);
    }
    validateStateHasNotChangedSinceAdded("The model was changed during the bind call.", position);
  }

  /**
   * Register a listener that will be called when this model is bound to a view.
   * <p>
   * The listener will contribute to this model's hashCode state per the {@link
   * com.airbnb.epoxy.EpoxyAttribute.Option#DoNotHash} rules.
   * <p>
   * You may clear the listener by setting a null value, or by calling {@link #reset()} */
  public ShiftEpoxyModel_ onBind(OnModelBoundListener<ShiftEpoxyModel_, ShiftEpoxyModel.ShiftViewHolder> listener) {
    onMutation();
    this.onModelBoundListener_epoxyGeneratedModel = listener;
    return this;
  }

  @Override
  public void unbind(ShiftEpoxyModel.ShiftViewHolder object) {
    super.unbind(object);
    if (onModelUnboundListener_epoxyGeneratedModel != null) {
      onModelUnboundListener_epoxyGeneratedModel.onModelUnbound(this, object);
    }
  }

  /**
   * Register a listener that will be called when this model is unbound from a view.
   * <p>
   * The listener will contribute to this model's hashCode state per the {@link
   * com.airbnb.epoxy.EpoxyAttribute.Option#DoNotHash} rules.
   * <p>
   * You may clear the listener by setting a null value, or by calling {@link #reset()} */
  public ShiftEpoxyModel_ onUnbind(OnModelUnboundListener<ShiftEpoxyModel_, ShiftEpoxyModel.ShiftViewHolder> listener) {
    onMutation();
    this.onModelUnboundListener_epoxyGeneratedModel = listener;
    return this;
  }

  public ShiftEpoxyModel_ day(@NotNull String day) {
    onMutation();
    super.setDay(day);
    return this;
  }

  @NotNull
  public String day() {
    return super.getDay();
  }

  public ShiftEpoxyModel_ dayRes(@Nullable @StringRes Integer dayRes) {
    onMutation();
    super.setDayRes(dayRes);
    return this;
  }

  @Nullable
  @StringRes
  public Integer dayRes() {
    return super.getDayRes();
  }

  public ShiftEpoxyModel_ beginDate(@NotNull Date beginDate) {
    onMutation();
    super.setBeginDate(beginDate);
    return this;
  }

  @NotNull
  public Date beginDate() {
    return super.getBeginDate();
  }

  public ShiftEpoxyModel_ endDate(@NotNull Date endDate) {
    onMutation();
    super.setEndDate(endDate);
    return this;
  }

  @NotNull
  public Date endDate() {
    return super.getEndDate();
  }

  @Override
  public ShiftEpoxyModel_ id(long id) {
    super.id(id);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ id(@NonNull Number... arg0) {
    super.id(arg0);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ id(long id1, long id2) {
    super.id(id1, id2);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ id(@NonNull CharSequence arg0) {
    super.id(arg0);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ id(@NonNull CharSequence arg0, @NonNull CharSequence... arg1) {
    super.id(arg0, arg1);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ id(@NonNull CharSequence arg0, long arg1) {
    super.id(arg0, arg1);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ layout(@LayoutRes int arg0) {
    super.layout(arg0);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ spanSizeOverride(@android.support.annotation.Nullable EpoxyModel.SpanSizeOverrideCallback arg0) {
    super.spanSizeOverride(arg0);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ show() {
    super.show();
    return this;
  }

  @Override
  public ShiftEpoxyModel_ show(boolean show) {
    super.show(show);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ hide() {
    super.hide();
    return this;
  }

  @Override
  protected ShiftEpoxyModel.ShiftViewHolder createNewHolder() {
    return new ShiftEpoxyModel.ShiftViewHolder();
  }

  @Override
  @LayoutRes
  protected int getDefaultLayout() {
    return R.layout.ui_recyclerview_epoxy_shift;
  }

  @Override
  public ShiftEpoxyModel_ reset() {
    onModelBoundListener_epoxyGeneratedModel = null;
    onModelUnboundListener_epoxyGeneratedModel = null;
    super.setDay(null);
    super.setDayRes(null);
    super.setBeginDate(null);
    super.setEndDate(null);
    super.reset();
    return this;
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (!(o instanceof ShiftEpoxyModel_)) {
      return false;
    }
    if (!super.equals(o)) {
      return false;
    }
    ShiftEpoxyModel_ that = (ShiftEpoxyModel_) o;
    if (((onModelBoundListener_epoxyGeneratedModel == null) != (that.onModelBoundListener_epoxyGeneratedModel == null))) {
      return false;
    }
    if (((onModelUnboundListener_epoxyGeneratedModel == null) != (that.onModelUnboundListener_epoxyGeneratedModel == null))) {
      return false;
    }
    if ((getDay() != null ? !getDay().equals(that.getDay()) : that.getDay() != null)) {
      return false;
    }
    if ((getDayRes() != null ? !getDayRes().equals(that.getDayRes()) : that.getDayRes() != null)) {
      return false;
    }
    if ((getBeginDate() != null ? !getBeginDate().equals(that.getBeginDate()) : that.getBeginDate() != null)) {
      return false;
    }
    if ((getEndDate() != null ? !getEndDate().equals(that.getEndDate()) : that.getEndDate() != null)) {
      return false;
    }
    return true;
  }

  @Override
  public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + (onModelBoundListener_epoxyGeneratedModel != null ? 1 : 0);
    result = 31 * result + (onModelUnboundListener_epoxyGeneratedModel != null ? 1 : 0);
    result = 31 * result + (getDay() != null ? getDay().hashCode() : 0);
    result = 31 * result + (getDayRes() != null ? getDayRes().hashCode() : 0);
    result = 31 * result + (getBeginDate() != null ? getBeginDate().hashCode() : 0);
    result = 31 * result + (getEndDate() != null ? getEndDate().hashCode() : 0);
    return result;
  }

  @Override
  public String toString() {
    return "ShiftEpoxyModel_{" +
        "day=" + getDay() +
        ", dayRes=" + getDayRes() +
        ", beginDate=" + getBeginDate() +
        ", endDate=" + getEndDate() +
        "}" + super.toString();
  }
}

With Version 2.16.2 it looks like this:

/**
 * Generated file. Do not modify! */
@Metadata(
    mv = {
        1,
        1,
        10
    },
    bv = {
        1,
        0,
        2
    },
    k = 1,
    d1 = "\u00000\n"
            + "\u0002\u0018\u0002\n"
            + "\u0002\u0018\u0002\n"
            + "\u0002\u0018\u0002\n"
            + "\u0002\b\u0002\n"
            + "\u0002\u0018\u0002\n"
            + "\u0002\b\u0005\n"
            + "\u0002\u0010\u000e\n"
            + "\u0002\b\u0005\n"
            + "\u0002\u0010\b\n"
            + "\u0002\b\t\n"
            + "\u0002\u0010\u0002\n"
            + "\u0002\b\u0003\b'\u0018\u00002\b\u0012\u0004\u0012\u00020\u00020\u0001:\u0001\u001dB\u0005¢\u0006\u0002\u0010\u0003J\u0010\u0010\u001a\u001a\u00020\u001b2\u0006\u0010\u001c\u001a\u00020\u0002H\u0016R\u001e\u0010\u0004\u001a\u00020\u00058\u0006@\u0006X\u0087\u000e¢\u0006\u000e\n"
            + "\u0000\u001a\u0004\b\u0006\u0010\u0007\"\u0004\b\b\u0010\tR\u001e\u0010\n"
            + "\u001a\u00020\u000b8\u0006@\u0006X\u0087\u000e¢\u0006\u000e\n"
            + "\u0000\u001a\u0004\b\f\u0010\r\"\u0004\b\u000e\u0010\u000fR\"\u0010\u0010\u001a\u0004\u0018\u00010\u00118\u0006@\u0006X\u0087\u000e¢\u0006\u0010\n"
            + "\u0002\u0010\u0016\u001a\u0004\b\u0012\u0010\u0013\"\u0004\b\u0014\u0010\u0015R\u001e\u0010\u0017\u001a\u00020\u00058\u0006@\u0006X\u0087\u000e¢\u0006\u000e\n"
            + "\u0000\u001a\u0004\b\u0018\u0010\u0007\"\u0004\b\u0019\u0010\t¨\u0006\u001e",
    d2 = {
        "Lzenjob/android/ui/recyclerview/epoxyitems/ShiftEpoxyModel;",
        "Lcom/airbnb/epoxy/EpoxyModelWithHolder;",
        "Lzenjob/android/ui/recyclerview/epoxyitems/ShiftEpoxyModel$ShiftViewHolder;",
        "()V",
        "beginDate",
        "Ljava/util/Date;",
        "getBeginDate",
        "()Ljava/util/Date;",
        "setBeginDate",
        "(Ljava/util/Date;)V",
        "day",
        "",
        "getDay",
        "()Ljava/lang/String;",
        "setDay",
        "(Ljava/lang/String;)V",
        "dayRes",
        "",
        "getDayRes",
        "()Ljava/lang/Integer;",
        "setDayRes",
        "(Ljava/lang/Integer;)V",
        "Ljava/lang/Integer;",
        "endDate",
        "getEndDate",
        "setEndDate",
        "bind",
        "",
        "holder",
        "ShiftViewHolder",
        "recyclerview_debug"
    }
)
public class ShiftEpoxyModel_ extends ShiftEpoxyModel implements GeneratedModel<ShiftEpoxyModel.ShiftViewHolder>, ShiftEpoxyModelBuilder {
  private OnModelBoundListener<ShiftEpoxyModel_, ShiftEpoxyModel.ShiftViewHolder> onModelBoundListener_epoxyGeneratedModel;

  private OnModelUnboundListener<ShiftEpoxyModel_, ShiftEpoxyModel.ShiftViewHolder> onModelUnboundListener_epoxyGeneratedModel;

  public ShiftEpoxyModel_() {
    super();
  }

  @Override
  public void addTo(EpoxyController controller) {
    super.addTo(controller);
    addWithDebugValidation(controller);
  }

  @Override
  public void handlePreBind(final EpoxyViewHolder holder,
      final ShiftEpoxyModel.ShiftViewHolder object, final int position) {
    validateStateHasNotChangedSinceAdded("The model was changed between being added to the controller and being bound.", position);
  }

  @Override
  public void handlePostBind(final ShiftEpoxyModel.ShiftViewHolder object, int position) {
    if (onModelBoundListener_epoxyGeneratedModel != null) {
      onModelBoundListener_epoxyGeneratedModel.onModelBound(this, object, position);
    }
    validateStateHasNotChangedSinceAdded("The model was changed during the bind call.", position);
  }

  /**
   * Register a listener that will be called when this model is bound to a view.
   * <p>
   * The listener will contribute to this model's hashCode state per the {@link
   * com.airbnb.epoxy.EpoxyAttribute.Option#DoNotHash} rules.
   * <p>
   * You may clear the listener by setting a null value, or by calling {@link #reset()} */
  public ShiftEpoxyModel_ onBind(OnModelBoundListener<ShiftEpoxyModel_, ShiftEpoxyModel.ShiftViewHolder> listener) {
    onMutation();
    this.onModelBoundListener_epoxyGeneratedModel = listener;
    return this;
  }

  @Override
  public void unbind(ShiftEpoxyModel.ShiftViewHolder object) {
    super.unbind(object);
    if (onModelUnboundListener_epoxyGeneratedModel != null) {
      onModelUnboundListener_epoxyGeneratedModel.onModelUnbound(this, object);
    }
  }

  /**
   * Register a listener that will be called when this model is unbound from a view.
   * <p>
   * The listener will contribute to this model's hashCode state per the {@link
   * com.airbnb.epoxy.EpoxyAttribute.Option#DoNotHash} rules.
   * <p>
   * You may clear the listener by setting a null value, or by calling {@link #reset()} */
  public ShiftEpoxyModel_ onUnbind(OnModelUnboundListener<ShiftEpoxyModel_, ShiftEpoxyModel.ShiftViewHolder> listener) {
    onMutation();
    this.onModelUnboundListener_epoxyGeneratedModel = listener;
    return this;
  }

  public ShiftEpoxyModel_ day(@NotNull String day) {
    onMutation();
    super.setDay(day);
    return this;
  }

  @NotNull
  public String day() {
    return super.getDay();
  }

  public ShiftEpoxyModel_ dayRes(@Nullable @StringRes Integer dayRes) {
    onMutation();
    super.setDayRes(dayRes);
    return this;
  }

  @Nullable
  @StringRes
  public Integer dayRes() {
    return super.getDayRes();
  }

  public ShiftEpoxyModel_ beginDate(@NotNull Date beginDate) {
    onMutation();
    super.setBeginDate(beginDate);
    return this;
  }

  @NotNull
  public Date beginDate() {
    return super.getBeginDate();
  }

  public ShiftEpoxyModel_ endDate(@NotNull Date endDate) {
    onMutation();
    super.setEndDate(endDate);
    return this;
  }

  @NotNull
  public Date endDate() {
    return super.getEndDate();
  }

  @Override
  public ShiftEpoxyModel_ id(long id) {
    super.id(id);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ id(@android.support.annotation.Nullable Number... arg0) {
    super.id(arg0);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ id(long id1, long id2) {
    super.id(id1, id2);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ id(@android.support.annotation.Nullable CharSequence arg0) {
    super.id(arg0);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ id(@android.support.annotation.Nullable CharSequence arg0,
      @android.support.annotation.Nullable CharSequence... arg1) {
    super.id(arg0, arg1);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ id(@android.support.annotation.Nullable CharSequence arg0, long arg1) {
    super.id(arg0, arg1);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ layout(@LayoutRes int arg0) {
    super.layout(arg0);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ spanSizeOverride(@android.support.annotation.Nullable EpoxyModel.SpanSizeOverrideCallback arg0) {
    super.spanSizeOverride(arg0);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ show() {
    super.show();
    return this;
  }

  @Override
  public ShiftEpoxyModel_ show(boolean show) {
    super.show(show);
    return this;
  }

  @Override
  public ShiftEpoxyModel_ hide() {
    super.hide();
    return this;
  }

  @Override
  protected ShiftEpoxyModel.ShiftViewHolder createNewHolder() {
    return new ShiftEpoxyModel.ShiftViewHolder();
  }

  @Override
  @LayoutRes
  protected int getDefaultLayout() {
    return R.layout.ui_recyclerview_epoxy_shift;
  }

  @Override
  public ShiftEpoxyModel_ reset() {
    onModelBoundListener_epoxyGeneratedModel = null;
    onModelUnboundListener_epoxyGeneratedModel = null;
    super.setDay(null);
    super.setDayRes(null);
    super.setBeginDate(null);
    super.setEndDate(null);
    super.reset();
    return this;
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (!(o instanceof ShiftEpoxyModel_)) {
      return false;
    }
    if (!super.equals(o)) {
      return false;
    }
    ShiftEpoxyModel_ that = (ShiftEpoxyModel_) o;
    if (((onModelBoundListener_epoxyGeneratedModel == null) != (that.onModelBoundListener_epoxyGeneratedModel == null))) {
      return false;
    }
    if (((onModelUnboundListener_epoxyGeneratedModel == null) != (that.onModelUnboundListener_epoxyGeneratedModel == null))) {
      return false;
    }
    if ((getDay() != null ? !getDay().equals(that.getDay()) : that.getDay() != null)) {
      return false;
    }
    if ((getDayRes() != null ? !getDayRes().equals(that.getDayRes()) : that.getDayRes() != null)) {
      return false;
    }
    if ((getBeginDate() != null ? !getBeginDate().equals(that.getBeginDate()) : that.getBeginDate() != null)) {
      return false;
    }
    if ((getEndDate() != null ? !getEndDate().equals(that.getEndDate()) : that.getEndDate() != null)) {
      return false;
    }
    return true;
  }

  @Override
  public int hashCode() {
    int result = super.hashCode();
    result = 31 * result + (onModelBoundListener_epoxyGeneratedModel != null ? 1 : 0);
    result = 31 * result + (onModelUnboundListener_epoxyGeneratedModel != null ? 1 : 0);
    result = 31 * result + (getDay() != null ? getDay().hashCode() : 0);
    result = 31 * result + (getDayRes() != null ? getDayRes().hashCode() : 0);
    result = 31 * result + (getBeginDate() != null ? getBeginDate().hashCode() : 0);
    result = 31 * result + (getEndDate() != null ? getEndDate().hashCode() : 0);
    return result;
  }

  @Override
  public String toString() {
    return "ShiftEpoxyModel_{" +
        "day=" + getDay() +
        ", dayRes=" + getDayRes() +
        ", beginDate=" + getBeginDate() +
        ", endDate=" + getEndDate() +
        "}" + super.toString();
  }
}
elihart commented 6 years ago

Is Cannot create an instance of an abstract class being thrown at runtime? Can you provide more of the stack trace?

@shaishavgandhi05 is right that both approaches instantiate the model the same way.

pwillmann commented 6 years ago

It's thrown during build. I tried clean rebuild as well as restarting and clearing the cache of Android Studio. Always the same result.

Will send more of the stack trace tomorrow :).

I think there was another error about the end / start date.

pwillmann commented 6 years ago

Okay so its used like this:

 carousel {
                id("shifts-carousel")
                withModelsFrom(data.offer!!.shifts) {
                    ShiftEpoxyModel_()
                            .id(it.toString())
                            .beginDate(it.beginDate)
                            .endDate(it.endDate)
                }
                padding(Carousel.Padding(data.shiftsPadding.toInt(), data.shiftsItemSpacing.toInt()))
            }

And throws Cannot create an instance of an abstract class for the ShiftEpoxyModel_() line and Unresolved reference: beginDate for the .beginDate(it.beginDate) line.

elihart commented 6 years ago

What version of Kotlin are you using? Are you getting this error everywhere or only for this model?

It's possible that the kotlin metadata annotation on the generated model is confusing the compiler - I'll remove that, it shouldn't be included

elihart commented 6 years ago

This may fix it https://github.com/airbnb/epoxy/pull/523

pwillmann commented 6 years ago

I tried with Kotlin Version 1.2.51 and 61, both with the same result.

Also the same problem with every model, as soon as i switch from the extension function to the generated ...Model_() class. (ie from spaceDivider to SpaceDividerEpoxyModel_().id("123")....

Maybe your pr will fix it, will try as soon as I can. Thanks!

elihart commented 6 years ago

2.17.0 is pushed and should be available in a few minutes. Please let me know if that works

pwillmann commented 6 years ago

Thanks a lot @elihart ! Works perfectly now :D