airbnb / epoxy

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

Tmp detached view should be removed from RecyclerView before it can be recycled #689

Closed haroldadmin closed 5 years ago

haroldadmin commented 5 years ago

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.

elihart commented 5 years ago

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

haroldadmin commented 5 years ago

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.

elihart commented 5 years ago

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

haroldadmin commented 5 years ago

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?

haroldadmin commented 5 years ago

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

fgiris commented 5 years ago

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.

AaronWoo1988 commented 4 years ago

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?

premacck commented 4 years ago

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!

AndroidDeveloperLB commented 4 years ago

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.

csguys commented 4 years ago

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

nidhi88 commented 4 years ago

worked for me! Thanks

AnkitKumarMaurya commented 2 years ago

HI I am also getting same issue my log is

java.lang.IllegalArgumentException: Tmp detached view should be removed from RecyclerView before it can be recycled: LoaderImageviewHolder{ce8765a position=6 id=-1, oldPos=-1, pLpos:-1 tmpDetached no parent} androidx.recyclerview.widget.RecyclerView{1b5e33f VFED..... ......ID 55,0-1025,1529 #7f0a02d8 app:id/image_home_rv}, adapter:app.com.brochill.ui.fragments.images.adapters.ImageHomeAdapter@769240c, layout:com.littlemango.stacklayoutmanager.StackLayoutManager@e083155, context:app.com.brochill.ui.activity.Dashboard@e29d523 at androidx.recyclerview.widget.RecyclerView$Recycler.recycleViewHolderInternal(RecyclerView.java:6620) at androidx.recyclerview.widget.RecyclerView.removeAnimatingView(RecyclerView.java:1525) at androidx.recyclerview.widget.RecyclerView$ItemAnimatorRestoreListener.onAnimationFinished(RecyclerView.java:13109) at androidx.recyclerview.widget.RecyclerView$ItemAnimator.dispatchAnimationFinished(RecyclerView.java:13611) at androidx.recyclerview.widget.SimpleItemAnimator.dispatchAddFinished(SimpleItemAnimator.java:302) at androidx.recyclerview.widget.DefaultItemAnimator$5.onAnimationEnd(DefaultItemAnimator.java:247) at android.view.ViewPropertyAnimator$AnimatorEventListener.onAnimationEnd(ViewPropertyAnimator.java:1122) at android.animation.Animator$AnimatorListener.onAnimationEnd(Animator.java:552) at android.animation.ValueAnimator.endAnimation(ValueAnimator.java:1232) at android.animation.ValueAnimator.doAnimationFrame(ValueAnimator.java:1474) at android.animation.AnimationHandler.doAnimationFrame(AnimationHandler.java:146) at android.animation.AnimationHandler.access$100(AnimationHandler.java:37) at android.animation.AnimationHandler$1.doFrame(AnimationHandler.java:54) at android.view.Choreographer$CallbackRecord.run(Choreographer.java:986) at android.view.Choreographer.doCallbacks(Choreographer.java:765) at android.view.Choreographer.doFrame(Choreographer.java:697) at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:967) at android.os.Handler.handleCallback(Handler.java:873) at android.os.Handler.dispatchMessage(Handler.java:99) at android.os.Looper.loop(Looper.java:214) at android.app.ActivityThread.main(ActivityThread.java:7156) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:494) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:975)

I am using StackLayoutManager (com.littlemango.stacklayoutmanager) for recycler view it happening only when I am fast scrolling

ebraminio commented 1 year ago

I was enabling a layout transition using

    this.layoutTransition = LayoutTransition().also {
        it.enableTransitionType(LayoutTransition.CHANGING)
        it.setAnimateParentHierarchy(false)
    }

on the parent of the recycler view, as that parent had other stuff which also needed layout transition, disabling that my issue is also gone, I wasn't expecting a stability crash but well.