RecyclerView.Adapter.notifyItemChanged() never passes payload to onBindViewHolder()
Asked Answered
S

4

35

I'm trying to update a ViewHolder in a RecyclerView without rebinding the entire thing. According to the docs, I should do this by calling RecyclerView.Adapter.notifyItemChanged(int position, Object payload), where payload is an arbitrary object that will be passed to RecyclerView.Adapter.onBindViewHolder(VH holder, int position, List<Object> payloads), where i'll be able to update the ViewHolder.

But when I try this, onBindViewHolder always receives an empty list. Between these two calls, the internal list of payloads is cleared. After setting breakpoints at the source code of RecyclerView, this happens because of a relayout, which eventually calls RecyclerView.ViewHolder.clearPayload()

Has anyone else managed to get this working? Is this a bug in the support library or is something I've done triggering a relayout between these two functions?

Here's the stack trace for when the payload is cleared:

"<1> main@831692616832" prio=5 runnable
  java.lang.Thread.State: RUNNABLE
      at android.support.v7.widget.RecyclerView$ViewHolder.clearPayload(RecyclerView.java:8524)
      at android.support.v7.widget.RecyclerView$ViewHolder.resetInternal(RecyclerView.java:8553)
      at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4544)
      at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:4461)
      at android.support.v7.widget.LayoutState.next(LayoutState.java:86)
      at android.support.v7.widget.StaggeredGridLayoutManager.fill(StaggeredGridLayoutManager.java:1423)
      at android.support.v7.widget.StaggeredGridLayoutManager.onLayoutChildren(StaggeredGridLayoutManager.java:610)
      at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:2847)
      at android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:3145)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.support.v4.widget.SwipeRefreshLayout.onLayout(SwipeRefreshLayout.java:581)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.widget.FrameLayout.onLayout(FrameLayout.java:448)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.widget.FrameLayout.onLayout(FrameLayout.java:448)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.widget.FrameLayout.onLayout(FrameLayout.java:448)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.widget.FrameLayout.onLayout(FrameLayout.java:448)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1671)
      at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1525)
      at android.widget.LinearLayout.onLayout(LinearLayout.java:1434)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.support.v4.widget.DrawerLayout.onLayout(DrawerLayout.java:1043)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.widget.FrameLayout.onLayout(FrameLayout.java:448)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1671)
      at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1525)
      at android.widget.LinearLayout.onLayout(LinearLayout.java:1434)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.widget.FrameLayout.onLayout(FrameLayout.java:448)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1671)
      at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1525)
      at android.widget.LinearLayout.onLayout(LinearLayout.java:1434)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.widget.FrameLayout.onLayout(FrameLayout.java:448)
      at android.view.View.layout(View.java:14289)
      at android.view.ViewGroup.layout(ViewGroup.java:4562)
      at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:1976)
      at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:1730)
      at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1004)
      at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:5481)
      at android.view.Choreographer$CallbackRecord.run(Choreographer.java:749)
      at android.view.Choreographer.doCallbacks(Choreographer.java:562)
      at android.view.Choreographer.doFrame(Choreographer.java:532)
      at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:735)
      at android.os.Handler.handleCallback(Handler.java:730)
      at android.os.Handler.dispatchMessage(Handler.java:92)
      at android.os.Looper.loop(Looper.java:137)
      at android.app.ActivityThread.main(ActivityThread.java:5103)
      at java.lang.reflect.Method.invokeNative(Method.java:-1)
      at java.lang.reflect.Method.invoke(Method.java:525)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:737)
      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:553)
      at dalvik.system.NativeStart.main(NativeStart.java:-1)
Scrimmage answered 8/9, 2015 at 16:47 Comment(1)
would you post the code for the adapter and the code that invoke notifyItemChanged ?Karyoplasm
S
39

RecyclerView by default creates another copy of the ViewHolder in order to fade the views into each other. This causes the problem because the old ViewHolder gets the payload but then the new one doesn't. So you need to explicitly tell it to reuse the old one:

DefaultItemAnimator animator = new DefaultItemAnimator() {
        @Override
        public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) {
            return true;
        }
    };
mRecyclerView.setItemAnimator(animator);
Sulfur answered 14/9, 2015 at 22:4 Comment(5)
I agree that this works and this is a better solution than setting the item animator to null. The default return value of the canReuseUpdatedViewHolder is supposed to be true. Android Source code and Android Documentation say that it returns true by default.Calvincalvina
Interesting, thanks for sharing. I got this solution from chatting with the guy who wrote RecyclerView at the Android Dev Summit back in November 2015, so maybe they've updated the code/documentation since then.Sulfur
Furthermore According to documentation: Adapter should not assume that the payload passed in notify methods will be received by onBindViewHolder(). For example when the view is not attached to the screen, the payload in notifyItemChange() will be simply dropped.Luminescence
When I inspect the code, the default implementation returns TRUE already public boolean canReuseUpdatedViewHolder(@NonNull ViewHolder viewHolder) { return true; }Accrue
But if I do not re-implement like @PatriciaLi said, the new viewholder is still created. I cannot understand why???Accrue
C
13

If you use item animator (by default it's enabled) then payload will be always empty. Look at this answer in android issue tracker:

This is how payload is designed the work. It is only passed to on bind "if we are rebinding to the same view holder". Same for data binding, if you are not binding to the same view holder, you cannot do the bind incrementally. In case of change animations, RV creates two copies of the View to crossfade so you are not binding to the same VH in postLayout.

Soon, we'll provide an API to run change animations within the same ViewHolder.

Therefore, until they share this new API we need to use something like this:

mRecyclerView.setItemAnimator(null);

edit: See @blindOSX comment, where he noticed that to enable payloads you can only disable animations of item change events.

edit2: Looks like they've updated this behaviour without notice about it. See updated @Patricia Li answer.

Corbet answered 31/10, 2015 at 12:1 Comment(1)
Payloads will work also if you use an item animator but disable only the animations of item change events with: ((SimpleItemAnimator) getItemAnimator()).setSupportsChangeAnimations(false);Aimo
J
1

why not simply use RecyclerView.Adapter.notifyItemChanged(int position) the docs seems a little ambiguous while mentioning about

  RecyclerView.Adapter.notifyItemChanged(int position,Object payload)

Client can optionally pass a payload for partial change. These payloads will be merged and may be passed to adapter's onBindViewHolder(ViewHolder, int, List)

hence it is not guaranteed that you will receive the List in your onBindViewHolder

Journeywork answered 8/9, 2015 at 18:13 Comment(1)
That explains why randomly the payload isn't reliable.Redbreast
F
1

In some case ,it worked with notifyItemRangeChanged(getItemRealCount(), 1).

Fluoric answered 5/8, 2016 at 5:54 Comment(1)
eh,at first ,you should setItemAnimator (null),or set your specified adapter like which overwrite @Override public boolean canReuseUpdatedViewHolder(RecyclerView.ViewHolder viewHolder) { return true; },or there is an IllegalArgumentExceptionFluoric

© 2022 - 2024 — McMap. All rights reserved.