Reputation: 383
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
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
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
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
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