RecyclerView.ItemDecoration doesn't update after item is removed from RecyclerView.Adapter
I solved this by changing my removeItemAt()
method as follows:
private void removeItemAt(int position) {
if (list.size() > 0) {
list.remove(position);
notifyItemRemoved(position);
if (position != 0) {
notifyItemChanged(position - 1, Boolean.FALSE);
}
}
}
The second argument to notifyItemChanged()
is the key. It can be literally any object, but I chose Boolean.FALSE
because it's a "well-known" object and because it conveys a little bit of intent: I don't want animations to run on the item I'm changing.
Why it works
It turns out that there's a second onBindViewHolder()
method defined in RecyclerView.Adapter
that I'd never seen before. From the source code:
public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
onBindViewHolder(holder, position);
}
Excerpted from that method's documentation:
The
payloads
parameter is a merge list fromnotifyItemChanged(int, Object)
ornotifyItemRangeChanged(int, int, Object)
. If thepayloads
list is not empty, theViewHolder
is currently bound to old data andAdapter
may run an efficient partial update using the payload info. If the payload is empty,Adapter
must run a full bind.
In other words, by passing something (anything) as a payload in notifyItemChanged()
, we're telling the system that we want to perform only a "partial update" (in situations where that's possible).
So, sure, we've instructed the system to perform a partial update... but how does that stop the flickering caused by simply calling notifyItemChanged(position - 1)
? It has to do with the RecyclerView.ItemAnimator
that's attached to all RecyclerView
s by default: DefaultItemAnimator
.
DefaultItemAnimator
's source code includes this method:
@Override
public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder,
@NonNull List<Object> payloads) {
return !payloads.isEmpty() || super.canReuseUpdatedViewHolder(viewHolder, payloads);
}
You'll notice that this method also takes a List<Object> payloads
parameter. This is the same list of payloads as used in the other onBindViewHolder()
call mentioned above.
Because we passed a payload argument, this method will return true
. And since the animator is now told that it can reuse the already-existing ViewHolder
for our "changed" item, it doesn't tear it down and create a new one (or reuse a recycled one)... which stops the default fade animation on the changed item from running!
This is a too late answer, but I will answer because I have the same problem. Try call RecyclerView.invalidateItemDecorations() before insert/update/remove data.
ListAdapter solution:
listAdapter.submitList(newList) {
handler.post { // or just "post" if you're inside View
recyclerView.invalidateItemDecorations()
}
}
The key is new submitList(list, commitCallback)
method from latest RecyclerView release. It allows you to call invalidateItemDecorations()
after ListAdapter will apply all changes to RecyclerView.
I also had to add post {}
call to make it work - without it decorations invalidated too early, and nothing happens.