epoxy: Random IllegalStateException when using WrappedEpoxyCheckedChangeListener

I’m using epoxy 3.6.0 with databinding.

My layout looks like this:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">

    <data>

        <variable
            name="text"
            type="Integer" />

        <variable
            name="checked"
            type="Boolean" />

        <variable
            name="checkedChangeListener"
            type="android.widget.CompoundButton.OnCheckedChangeListener" />

    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:paddingStart="@dimen/keyline_5"
        android:paddingEnd="@dimen/keyline_5"
        android:paddingTop="@dimen/keyline_4"
        android:paddingBottom="@dimen/keyline_4"
        android:foreground="?selectableItemBackground">

        <TextView
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:layout_marginEnd="32dp"
            style="@style/TextAppearance.App.SimpleItemTitle"
            android:text="@{text}"
            android:layout_weight="1"/>

        <androidx.appcompat.widget.SwitchCompat
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            android:checked="@{checked}"
            app:onCheckedChangeListener="@{checkedChangeListener}"/>

    </LinearLayout>

</layout>

The relevant part of my AsyncEpoxyController(enableAsyncModelBuilding = false, enableAsyncDiffing = true)'s (but also happens with a regular EpoxyController) buildModels() function looks like this:

    switchNoIconItem {
            id("someSwitch")
            text(R.string.switch_text)
            checked(settings.isSomeSettingEnabled)
            checkedChangeListener { _, _, _, isChecked, _ ->
                // handle check change
            }
        }

Sometimes (seemingly purely randomly) I the app is crashing with the following exception:

java.lang.IllegalStateException: Could not find RecyclerView holder for clicked view
        at com.airbnb.epoxy.WrappedEpoxyModelCheckedChangeListener.onCheckedChanged(WrappedEpoxyModelCheckedChangeListener.java:31)
        at android.widget.CompoundButton.setChecked(CompoundButton.java:180)
        at androidx.appcompat.widget.SwitchCompat.setChecked(SwitchCompat.java:1064)
        at androidx.databinding.adapters.CompoundButtonBindingAdapter.setChecked(CompoundButtonBindingAdapter.java:44)
        at de.whisp.clear.databinding.ItemLayoutSwitchNoIconItemBindingImpl.executeBindings(ItemLayoutSwitchNoIconItemBindingImpl.java:249)
        at androidx.databinding.ViewDataBinding.executeBindingsInternal(ViewDataBinding.java:472)
        at androidx.databinding.ViewDataBinding.executePendingBindings(ViewDataBinding.java:444)
        at com.airbnb.epoxy.DataBindingEpoxyModel.bind(DataBindingEpoxyModel.java:56)
        at com.airbnb.epoxy.DataBindingEpoxyModel.bind(DataBindingEpoxyModel.java:36)
        at com.airbnb.epoxy.EpoxyViewHolder.bind(EpoxyViewHolder.java:58)
        at com.airbnb.epoxy.BaseEpoxyAdapter.onBindViewHolder(BaseEpoxyAdapter.java:98)
        at com.airbnb.epoxy.BaseEpoxyAdapter.onBindViewHolder(BaseEpoxyAdapter.java:15)
        at androidx.recyclerview.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:7075)
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryBindViewHolderByDeadline(RecyclerView.java:5991)
        at androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:6258)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6097)
        at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:6093)
        at androidx.recyclerview.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2303)
        at androidx.recyclerview.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1627)
        at androidx.recyclerview.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1587)
        at androidx.recyclerview.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:665)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:4115)
        at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3832)
        at androidx.recyclerview.widget.RecyclerView.consumePendingUpdateOperations(RecyclerView.java:1900)
        at androidx.recyclerview.widget.RecyclerView$1.run(RecyclerView.java:416)
        at android.view.Choreographer$CallbackRecord.run(Choreographer.java:966)
        at android.view.Choreographer.doCallbacks(Choreographer.java:790)
        at android.view.Choreographer.doFrame(Choreographer.java:721)
        at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:951)
        at android.os.Handler.handleCallback(Handler.java:883)
        at android.os.Handler.dispatchMessage(Handler.java:100)
        at android.os.Looper.loop(Looper.java:214)
        at android.app.ActivityThread.main(ActivityThread.java:7343)
        at java.lang.reflect.Method.invoke(Native Method)
        at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:492)
        at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:933)

I never saw this happening with the old WrappedEpoxyModelClickListener and I cannot reproduce it reliably - it just happens sometimes when clicking on the switch. Any idea what might cause this?

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Comments: 15 (6 by maintainers)

Most upvoted comments

There is a 3.8.0 now available on maven http://central.maven.org/maven2/com/airbnb/android/epoxy/3.8.0/ with this change

@elihart Thanks a lot, we really appreciate the efforts you are putting into this project! ❤️

No, I’ve just been waiting for two other issues to get fixed before releasing, but I’ll stop waiting and make a release right now, sorry for the long delay 😦

I’ll push a release this week when I have time

Thanks for reporting, this is indeed an issue.

It looks like the problem is that when the view is bound, if the check listener is set before the checked value is set, the initial binding of the checked value triggers a call to the listener.

The listener tries to look up which recyclerview item it is in, so it can do the nifty wrapping helper callback, and throws this error when it realized it isn’t fully bound yet.

I’m going to assume that the behavior we want is for the initial bind to not trigger the checked change callback (although your vanilla listener is likely doing that now) - it is wasteful and unnecessary because your data should be in the same state that was just bound.

So then the question is, how can we prevent this listener from calling back in the case where it is not yet bound.

The easy solution is to remove the error throwing and just ignore it

 public void onCheckedChanged(CompoundButton button, boolean isChecked) {
    EpoxyViewHolder epoxyHolder = ListenersUtils.getEpoxyHolderForChildView(button);
    if (epoxyHolder == null) {
      throw new IllegalStateException("Could not find RecyclerView holder for clicked view");
    }

I think this would be fine - we could potentially miss other errors caused by this bad state, but that doesn’t seem likely.

The other solution is to change the generated code to disable the checked change listener and reenable it after binding completes, but that complexity in the generated code concerns me.