wshelor
wshelor

Reputation: 383

RecyclerView and DiffUtil with animations crash with IndexOutOfBoundsException: Inconsistency detected

We have an app that can update based on push notifications. What we've found is that sometimes with RecyclerViews that use animations and DiffUtil, the animations crash the app. It seems internally the recyclerview is animating views while DiffUtil is operating on them, resulting in this exception:

 java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 0(offset:-1).state:12

I can make this reproduce consistently by building a recyclerview that uses diffutil and rapidly randomizing the values (10 changes per second). It usually takes a few seconds, so it's not consistent, but it eventually crashes. Disabling animations fixes it.

It seems to happen when a diffutil is fired before DefaultItemAnimator.runPendingAnimations() completes. I pass a copy of the list to the diffutil each time I run it.

This is our diffutil:

class BindableDataDiffCallback(private val results: MutableList<DataClass>,
                           private val newResults: ArrayList<DataClass>) : DiffUtil.Callback() {
override fun getOldListSize() = results.size
override fun getNewListSize() = newResults.size

override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        results[oldItemPosition].getId() == newResults[newItemPosition].getId()
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int) =
        results[oldItemPosition].hashCode == newResults[newItemPosition].hashCode

}

This is the full stack trace:

java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid item position 0(offset:-1).state:12 androidx.recyclerview.widget.RecyclerView{8307495 VFED..... .F....ID 0,114-1080,1545 #7f0a0a5c app:id/staggeredGridView2}, androidx.recyclerview.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5923)
    at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5858)
    at androidx.recyclerview.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5854)
    at androidx.recyclerview.widget.LayoutState.next(LayoutState.java:100)
    at androidx.recyclerview.widget.StaggeredGridLayoutManager.fill(StaggeredGridLayoutManager.java:1609)
    at androidx.recyclerview.widget.StaggeredGridLayoutManager.scrollBy(StaggeredGridLayoutManager.java:2182)
    at androidx.recyclerview.widget.StaggeredGridLayoutManager.fixEndGap(StaggeredGridLayoutManager.java:1420)
    at androidx.recyclerview.widget.StaggeredGridLayoutManager.onLayoutChildren(StaggeredGridLayoutManager.java:698)
    at androidx.recyclerview.widget.StaggeredGridLayoutManager.onLayoutChildren(StaggeredGridLayoutManager.java:605)
    at androidx.recyclerview.widget.RecyclerView.dispatchLayoutStep1(RecyclerView.java:3875)
    at androidx.recyclerview.widget.RecyclerView.dispatchLayout(RecyclerView.java:3639)
    at androidx.recyclerview.widget.RecyclerView.onLayout(RecyclerView.java:4194)
    at android.view.View.layout(View.java:19590)
    at android.view.ViewGroup.layout(ViewGroup.java:6053)
    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)
    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)
    at android.widget.LinearLayout.onLayout(LinearLayout.java:1544)
    at android.view.View.layout(View.java:19590)
    at android.view.ViewGroup.layout(ViewGroup.java:6053)
    at androidx.viewpager.widget.ViewPager.onLayout(ViewPager.java:1775)
    at android.view.View.layout(View.java:19590)
    at android.view.ViewGroup.layout(ViewGroup.java:6053)
    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)
    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)
    at android.widget.LinearLayout.onLayout(LinearLayout.java:1544)
    at android.view.View.layout(View.java:19590)
    at android.view.ViewGroup.layout(ViewGroup.java:6053)
    at com.google.android.material.appbar.HeaderScrollingViewBehavior.layoutChild(HeaderScrollingViewBehavior.java:142)
    at com.google.android.material.appbar.ViewOffsetBehavior.onLayoutChild(ViewOffsetBehavior.java:41)
    at com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior.onLayoutChild(AppBarLayout.java:1556)
    at androidx.coordinatorlayout.widget.CoordinatorLayout.onLayout(CoordinatorLayout.java:888)
    at android.view.View.layout(View.java:19590)
    at android.view.ViewGroup.layout(ViewGroup.java:6053)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
    at android.view.View.layout(View.java:19590)
    at android.view.ViewGroup.layout(ViewGroup.java:6053)
    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)
    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)
    at android.widget.LinearLayout.onLayout(LinearLayout.java:1544)
    at android.view.View.layout(View.java:19590)
    at android.view.ViewGroup.layout(ViewGroup.java:6053)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
    at android.view.View.layout(View.java:19590)
    at android.view.ViewGroup.layout(ViewGroup.java:6053)
    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1791)
    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1635)
    at android.view.View.layout(View.java:19590)
    at android.view.ViewGroup.layout(ViewGroup.java:6053)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
    at com.android.internal.policy.DecorView.onLayout(DecorView.java:758)
    at android.view.View.layout(View.java:19590)
    at android.view.ViewGroup.layout(ViewGroup.java:6053)
    at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2484)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2200)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1386)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6733)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:911)
    at android.view.Choreographer.doCallbacks(Choreographer.java:723)
    at android.view.Choreographer.doFrame(Choreographer.java:658)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:897)
    at android.os.Handler.handleCallback(Handler.java:789)
    at android.os.Handler.dispatchMessage(Handler.java:98)
    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)

Has anyone had any luck with this?

Upvotes: 2

Views: 2254

Answers (4)

FESEVA
FESEVA

Reputation: 11

I had the same IndexOutOfBoundsException when performing clicks using DiffUtil.

I have tried different things and the one that solved the problem was extending from ListAdapter<YourItemDAO, YourViewHolder>(DiffUtilItemCallback)

I would say that the key is the getItem(position) method that get the current real item from the diffUtil list.

Apdapter code:

class YourItemAdapter(
    private val onClickYourItem: (item: YourItemDAO) -> Unit
) : ListAdapter<YourItemDAO, YourItemViewHolder>(DiffUtilItemCallback) {


    object DiffUtilItemCallback: DiffUtil.ItemCallback<YourItemDAO>() {
        override fun areItemsTheSame(oldItem: YourItemDAO, newItem: YourItemDAO): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: YourItemDAO, newItem: YourItemDAO): Boolean {
            return oldItem == newItem
        }

    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): YourItemViewHolder {
        val layoutInflater = LayoutInflater.from(parent.context)
        val yourItemBinding =
            YourItemBinding.inflate(layoutInflater, parent, false)
        return YourItemViewHolder(yourItemBinding)
    }

    override fun onBindViewHolder(holder: YourItemViewHolder, position: Int) {
        val item = getItem(position)
        holder.bind(item, onClickYourItem)
    }

    fun updateAll(newList: List<YourItemDAO>){
        submitList(newList)
    }

}

ViewHolder Code:

class YourItemViewHolder(private val binding: YourItemBinding): RecyclerView.ViewHolder(binding.root) {
    fun bind(item: YourItemDAO, onClickYourItem: (item: YourItemDAO) -> Unit) {
        // data binding
        binding.yourItem = item
        binding.yourItemCard.setOnClickListener {
               onClickYourItem(item)
        }
    }
}

I hope it helps 😃

Upvotes: 1

DevinM
DevinM

Reputation: 1322

Make sure, in your adapter's update method, that you are calculating the diff with the correct list for each param.

You haven't shared your adapter so I cannot confirm this HOWEVER your error is reproducible 100% if your new list is smaller than your old list (an item was removed) but your comparison is backwards.

What you may have been doing:

fun updateList(newData: List<DataClass>) {
    val diffResult = DiffUtil.calculateDiff(BindableDataDiffCallback(newData, oldData))
    diffResult.dispatchUpdatesTo(this)
}

What you should be doing:

fun updateList(newData: List<DataClass>) {
    val diffResult = DiffUtil.calculateDiff(BindableDataDiffCallback(oldData, newData))
    diffResult.dispatchUpdatesTo(this)
}

Upvotes: 0

Miklos Jakab
Miklos Jakab

Reputation: 2034

If by any chance it is related to some threading issue (according to your description it may be), you could look at the AsyncListDiffer instead of DiffUtil. The former uses the latter on a background thread, and notifies the ui. The usage is fairly straight forward: AsyncListDiffer

Upvotes: 0

Nermeen
Nermeen

Reputation: 15973

The issue may be related to the adapter. If you are using setHasStableIds(true); in the adapter constructor, you need to make sure that you are using stable ids.

Make sure that you override getItemId correctly in the adapter. It should be:

@Override
public long getItemId(int position) {
   return yourList == null ? 0 : yourList.get(position).getId();
}

And not:

@Override
public long getItemId(int position) {
     return position;
}

Upvotes: 1

Related Questions