84

I currently have a recycler view whose data updates every 5 secs. To update the data on the list, I am using

notifyItemChanged(position);
notifyItemRangeChanged(position, mList.size());

Each time I call notifyItemChanged(), the items on my recycler view update properly, however, it will blink because this causes onBindViewHolder to be called again. So it's as though it is a fresh load each time. How can I prevent this from happening, if possible?

portfoliobuilder
  • 7,556
  • 14
  • 76
  • 136
  • im using glide to load image, and try all answer to solve blinking but not working. then using Picasso solving the problem. and run smoothly – Muklas Aug 09 '19 at 22:56
  • Pay attention to this https://stackoverflow.com/a/32488059/1621111 – Konstantin Konopko Apr 23 '21 at 14:56
  • Please check this article: https://blog.undabot.com/recyclerview-time-to-animate-with-payloads-and-diffutil-4278beb8d4dd – Ievgen Apr 28 '21 at 13:54
  • I have a problem with this, i tried everything, setting supportChangeAnimations to false, using stableIds, diffutils, nothing worked, any other solution for this? – Limun Jun 30 '23 at 14:31

11 Answers11

146

RecyclerView has built in animations which usually add a nice polished effect. in your case you'll want to disable them:

((SimpleItemAnimator) mRecyclerView.getItemAnimator()).setSupportsChangeAnimations(false);

(The default recycler view animator should already be an instance of SimpleItemAnimator)

For Kotlin,

(mRecyclerView?.itemAnimator as SimpleItemAnimator).supportsChangeAnimations = false
Nick Cardoso
  • 20,807
  • 14
  • 73
  • 124
  • 14
    but I think this also disables default animations of methods such as notifyItemInserted –  Feb 22 '17 at 00:21
57

You can disable the item animations.

mRecyclerView.setItemAnimator(null);
Buddy
  • 595
  • 4
  • 4
  • 5
    this just seems to make the flickr faster than before, i'm using databinding. StableIds is sensible optimisation but you still have a flickr on the rows that are updating (they have radio button which change state but the rest of the view is unchanged so shouldn't filckr) – hmac Oct 29 '19 at 15:21
  • 1
    This is the best answer. Fixed a few bugs at once. – Master Mar 25 '21 at 02:13
22

The problem may not come from animation but from not stable id of the list item.

To use stable IDs, you need to:

  • setHasStableIds(true)
    In RecyclerView.Adapter, we need to set setHasStableIds(true); true means this adapter would publish a unique value as a key for item in data set. Adapter can use the key to indicate they are the same one or not after notifying data changed.

  • override getItemId(int position)
    Then we must override getItemId(int position), to return identified long for the item at position. We need to make sure there is no different item data with the same returned id.

The source of solution for that is here.

Sri Harsha Chilakapati
  • 11,744
  • 6
  • 50
  • 91
Marian Paździoch
  • 8,813
  • 10
  • 58
  • 103
9

If you want to keep RecyclerView's animation and avoid item flash in the same time, you can follow the steps below:

1. change view component status directly 2. change data in Adapter directly 3. don't need to refresh UI manually by calling notifyItemChanged()

Just change the view directly would not make change immediately, the better way is using payload to do it.

In your adapter:

override fun onBindViewHolder(
        holder: RecyclerView.ViewHolder,
        position: Int,
        payloads: MutableList<Any>
    ) {
        // Using payload to refresh partly
        if (payloads.isNullOrEmpty()) {
            // refresh all 
            onBindViewHolder(holder, position)
            return
        }
        // just change one component's status which would not "flash".
        (holder as YourHolder).apply{
            // make you view change
            // etc. checkBox.isChecked = true
        }
    }

And using it:


yourAdapter.notifyItemChanged(position, "sample_pay_load")

Here is notifyitemchanged's Doc

rosuh
  • 428
  • 4
  • 10
7

Use stableId in your adapter.

Call adapter.setHasStableIds(true) and override getItemId(int position) method in your adapter class.

Also, return some unique id from getItemId(int position) for each item. Don't just simply return position.

Harshit Jain
  • 142
  • 1
  • 4
  • 7
    Didn't change anything for me. When updating one item with a button, this item still flashes up – Andrew Dec 15 '20 at 15:35
5

This happening because Adapter is creating new ViewHolder and trying to animate between old and new ViewHolders. That's why all the "disable animations" answers technically work.

Instead you can just tell the RecyclerView to re-use Viewholders with canReuseUpdatedViewHolder(). See this answer to a similar question: https://stackoverflow.com/a/60427676/1650674

tir38
  • 9,810
  • 10
  • 64
  • 107
3

I read the below solution and implemented and it works fine on every device I tested.

  • Clone Default animator class.
  • In animateChange() method, comment below 3 lines,

    final float prevAlpha = oldHolder.itemView.getAlpha();
    .
    .
    oldHolder.itemView.setAlpha(prevAlpha);
    .
    .
    newHolder.itemView.setAlpha(0);
    
  • Set recyclerview item animator to that of your cloned class.

//Note: I do understand how the solution works with not changing the alpha value of new holder but this is not my solution, I read this on stackoverflow itself but for some reason could not find it anymore. Sharing this to help out fellow developers.

karan vs
  • 3,044
  • 4
  • 19
  • 26
2

In my case, neither any of above nor the answers from other stackoverflow questions having same problems worked.

Well, I was using custom animation each time the item gets clicked, for which I was calling notifyItemChanged(int position, Object Payload) to pass payload to my CustomAnimator class.

Notice, there are 2 onBindViewHolder(...) methods available in RecyclerView Adapter. onBindViewHolder(...) method having 3 parameters will always be called before onBindViewHolder(...) method having 2 parameters.

Generally, we always override the onBindViewHolder(...) method having 2 parameters and the main root of problem was I was doing the same, as each time notifyItemChanged(...) gets called, our onBindViewHolder(...) method will be called, in which I was loading my image in ImageView using Picasso, and this was the reason it was loading again regardless of its from memory or from internet. Until loaded, it was showing me the placeholder image, which was the reason of blinking for 1 sec whenever I click on the itemview.

Later, I also override another onBindViewHolder(...) method having 3 parameters. Here I check if the list of payloads is empty, then I return the super class implementation of this method, else if there are payloads, I am just setting the alpha value of the itemView of holder to 1.

And yay I got the solution to my problem after wasting a one full day sadly!

Here's my code for onBindViewHolder(...) methods:

onBindViewHolder(...) with 2 params:

@Override
public void onBindViewHolder(@NonNull RecyclerAdapter.ViewHolder viewHolder, int position) {
            Movie movie = movies.get(position);

            Picasso.with(context)
                    .load(movie.getImageLink())
                    .into(viewHolder.itemView.posterImageView);
    }

onBindViewHolder(...) with 3 params:

@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads);
        } else {
            holder.itemView.setAlpha(1);
        }
    }

Here's the code of method I was calling in onClickListener of viewHolder's itemView in onCreateViewHolder(...):

private void onMovieClick(int position, Movie movie) {
        Bundle data = new Bundle();
        data.putParcelable("movie", movie);

        // This data(bundle) will be passed as payload for ItemHolderInfo in our animator class
        notifyItemChanged(position, data);
    }

Note: You can get this position by calling getAdapterPosition() method of your viewHolder from onCreateViewHolder(...).

I have also overridden getItemId(int position) method as follows:

@Override
public long getItemId(int position) {
    Movie movie = movies.get(position);
    return movie.getId();
}

and called setHasStableIds(true); on my adapter object in activity.

Hope this helps if none of the answers above work!

1

Additionaly, try to run each notifyItemChanged in a separate launched coroutine job inside Main scope. It worked perfectly for me removing blinking significantly.

for (i in 0 until adapter.itemCount) {
    CoroutineScope(Main).launch {
        adapter.notifyItemChanged(i)
    }
}
Boken
  • 4,825
  • 10
  • 32
  • 42
0

If only the content inside the view needs changes then would suggest using notifyItemChanged with payload. If the whole view type changes then notifyItemChanged without payload will work but will re-render the view giving a flicker effect. Here is the code.

adapter.notifyItemChanged(position, "CHANGE_TEXT_VIEW_ITEM");

And then in the RecyclerViewAdapter override onBindViewHolder which has payload with it.

@Override
    public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List<Object> payloads) {
        if (payloads.isEmpty()) {
            super.onBindViewHolder(holder, position, payloads);
        } else {
            for (Object payload : payloads) {
                if (payload.equals("CHANGE_TEXT_VIEW_ITEM")) {
                    //you have holder and position to do relevant changes
                    }
            }
        }
    }

This will update the recycler items without any flicker. This can be achieved for notifyItemRangeChanged as well.

0

My RecyclerView would entirely redraw itself when I notified it that just one item had changed (NotifyItemChanged). I used the following:

val binding = ExRvItemBinding.inflate(layoutInflater)
        setContentView(binding.root)

    (binding.childRvId.itemAnimator as 
    SimpleItemAnimator).supportsChangeAnimations = false  
    binding.childRvId.itemAnimator = null

This seemed to fix the problem. Note that childRvId is my RecyclerView and that I am using Binding