Epoxy: Tmp detached view should be removed from RecyclerView before it can be recycled

Created on 16 Feb 2019  路  11Comments  路  Source: airbnb/epoxy

Hi.
I'm getting some random crashes in one particular screen of my app. The screen is a scrolling view of various movie details, and I use Epoxy to create it. This is the error reported in logcat:

java.lang.IllegalArgumentException: Tmp detached view should be removed from RecyclerView before it can be recycled

A complete stack trace can be found here

Some Googling suggest the following solutions:

  1. Setting 'setHasStableIds = false` on the recycler view adapter but this option obviously does not apply to Epoxy. source
  2. Removing custom item animators on the RecyclerView, but I'm not using any. source

Here's the Epoxy Model for which this error occurs:

@EpoxyModelClass(layout = R.layout.view_info_text)
abstract class InfoTextModel : EpoxyModelWithHolder<InfoTextModel.InfoTextViewHolder>() {

    @EpoxyAttribute
    lateinit var text: String

    override fun bind(holder: InfoTextViewHolder) {
        super.bind(holder)
        holder.textView.text = text
    }

    inner class InfoTextViewHolder : KotlinEpoxyHolder() {
        val textView by bind<TextView>(R.id.tvInfoText)
    }
}

Here's the XML file for this model:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/tvInfoText"
        style="@style/TextAppearance.MaterialComponents.Body2.EmptyViewTextStyle"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:minHeight="100dp"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        android:layout_marginTop="16dp"
        android:layout_marginBottom="16dp"
        android:layout_gravity="center"
        android:gravity="center"/>
</FrameLayout>

Here's the relevant code in the Epoxy Controller:

actorsList.takeIf { list -> !list.isNullOrEmpty() }?.forEach { actorResource ->
                ...
    } ?: infoText {
            id("cast-empty")
            text("This list seems to be empty")
            spanSizeOverride { totalSpanCount, _, _ -> totalSpanCount }
    }

If you need complete sources, they can be found in this package on my Github repo

I'm not sure why does this error occur, or how to solve it. This seems to be a recycler view specific issue, but in my app I'm using the EpoxyRecyclerView class.
Any help would be greatly appreciated.

Most helpful comment

I was also getting bugged by this issue in production, turns out you shouldn't use android:animateLayoutChanges="true" on the parent ViewGroup of the RecyclerView/EpoxyRecyclerView

After I removed that one line, everything runs like a peach!

All 11 comments

I'm not sure why this is happening, potentially a RecyclerView/GridLayoutManager bug

I'm not sure how to proceed here, @elihart. Since this could be an issue with IDs, should I try using @AutoModel for this layout?

It's especially hard to debug because this crash occurs seemingly randomly.

I would recommend enabling duplicate ID filtering and debug alerting so you are notified of duplicates and they don't make it into the final list

I've enabled Duplicate ID filtering and Debug alerting, but so far nothing seems out of the ordinary. This is the log output of the epoxy controller just before this crash occurred:

D/DetailsEpoxyController: Models built: 0.192ms
D/DetailsEpoxyController: Item range inserted. Start: 0 Count: 6
D/DetailsEpoxyController: Models diffed: 0.339ms
D/DetailsEpoxyController: Models built: 0.307ms
D/DetailsEpoxyController: Item range removed. Start: 5 Count: 1
D/DetailsEpoxyController: Item range inserted. Start: 5 Count: 1
D/DetailsEpoxyController: Item range removed. Start: 1 Count: 1
D/DetailsEpoxyController: Item range inserted. Start: 1 Count: 1
D/DetailsEpoxyController: Item range inserted. Start: 0 Count: 1
D/DetailsEpoxyController: Models diffed: 2.321ms
D/DetailsEpoxyController: Models built: 0.311ms
D/DetailsEpoxyController: Models diffed: 0.213ms
D/DetailsEpoxyController: Models built: 0.273ms
D/DetailsEpoxyController: Models diffed: 0.212ms
D/DetailsEpoxyController: Models built: 0.421ms
D/DetailsEpoxyController: Item range removed. Start: 4 Count: 1
D/DetailsEpoxyController: Item range inserted. Start: 4 Count: 1
D/DetailsEpoxyController: Models diffed: 0.484ms
D/DetailsEpoxyController: Models built: 0.550ms
D/DetailsEpoxyController: Item range removed. Start: 6 Count: 1
D/DetailsEpoxyController: Item range inserted. Start: 6 Count: 8
D/DetailsEpoxyController: Models diffed: 0.392ms

I have many network requests being done asynchronously, and when any one of their results gets back I update the epoxy controller with a new state. Hence so many Models built log outputs, I guess.

I found a Github issue on another RecyclerView library for this exact error here. Do you think this could be the problem here as well?

Closing this issue because this might not be an issue related to Epoxy, and there are no available answers.

I had the same issue. It was happening in Espresso tests and I have multiple api calls which updates the list. I would recommend canceling pending model build before requesting any model build and doing this may help to solve this issue.

In your controller

fun setList(data: List<TypedListItem>) {
        ....
        cancelPendingModelBuild()
        requestDelayedModelBuild(0)
    }

Make sure to call this in your fragment as epoxyController.setList(itemList) whenever an update occurs in the list.

Closing this issue because this might not be an issue related to Epoxy, and there are no available answers.

So, what is the resolution finally?

I was also getting bugged by this issue in production, turns out you shouldn't use android:animateLayoutChanges="true" on the parent ViewGroup of the RecyclerView/EpoxyRecyclerView

After I removed that one line, everything runs like a peach!

This issue is quite known:
https://stackoverflow.com/a/60024405/878126

I reported about this here:
https://issuetracker.google.com/issues/148720682

Basically I think it's RecyclerView not liking that you hide it while it's still animating.

I was also getting bugged by this issue in production, turns out you shouldn't use android:animateLayoutChanges="true" on the parent ViewGroup of the RecyclerView/EpoxyRecyclerView

After I removed that one line, everything runs like a peach!

Work for me as well

worked for me! Thanks

Was this page helpful?
0 / 5 - 0 ratings