2

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?

wshelor
  • 363
  • 2
  • 14
  • Generally, the `DiffUtil` callbacks compare old and new item on their IDs and structural differences with respect to their model class, why is your `DiffUtil` written like that? – Taseer Jul 26 '19 at 18:24
  • That's what is happening here. While the code in my non-test app is more complex, I wanted to simplify it so that we don't have a ton of extraneous comparisons in the question. The values for areItemsTheSame compares their IDs, areContentsTheSame ensures that they are in fact entirely the same item. – wshelor Jul 26 '19 at 18:30
  • What happens if you change your example callback to create copies of the lists at construction time? `val resultsCopy = results.toCollection(mutableListOf())` and then use `resultsCopy` instead of `results`? – Ben P. Jul 26 '19 at 19:37
  • We already do this. The constructor creates a new list each time. I'll edit that into the question. – wshelor Jul 26 '19 at 21:03

3 Answers3

1

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;
}
Nermeen
  • 15,883
  • 5
  • 59
  • 72
0

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

Miklos Jakab
  • 2,024
  • 1
  • 23
  • 31
0

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)
}
DevinM
  • 1,112
  • 1
  • 12
  • 29