Stop notifyDataSetChanged(), use DiffUtil to make your RecyclerView more silky

Table of Contents

  • scene introduction
  • DiffUtil
    • how to use
    • code analysis
  • ReyclerView.ListAdapter
    • Let’s see how to use it first
    • Source code analysis

Scene introduction

First introduce MVVM

I believe everyone is already very familiar with

ViewModel is responsible for providing data to View. Usually, we will observe the data changes of ViewModel, and refresh the UI when new data is received.

Usually we might write

//Observe data in the View layer
 mViewModel.observeData().observe(activity, Observer { data->
    //Submit data to adapter to update UI
    adapter. submitList(data)
 })

//Write this in Adapter
var mList: List<String>? = null
fun submitList(list: List<String>) {
    mList = list
    notifyDataSetChanged()
}

Suppose data1 and data2 are like this

val data1 = listOf("A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", " P", "Q", "R", "S", "T", "U", "V", "W", "S\ ", "Y", "Z")

val data2 = listOf( "K", "A", "b", "C", "Z", "E", "F", "G ", "H", "I", "J","L", "m","N","P","P" ,"Q","S","T","U","V","W","S","Y",\ "A","C")

The effect is probably like this

We can see that two different data data1, data2 have data movement, change (case change to change), increase, delete

But these changes are not displayed clearly, just a blunt change of view.

What if we want to have corresponding animations for the addition, deletion, and movement of each element?

Probably such an effect

We know that to have the animation effect of adding, deleting, changing and moving, we need to call RecyelerView.Adapter

public final void notifyItemInserted(int position)
  
public final void notifyItemRemoved(int position)
  
public final void notifyItemChanged(int position)
  
public final void notifyItemMoved(int fromPosition, int toPosition)

Then the question turns into how to quickly find out which element has been added, deleted, modified, or moved from data1 and data2

You can spend a little time thinking about this problem, this is actually a very complicated difference problem

but! Fortunately, Google officially provides us with DiffUtil to help us with this operation

Next introduce DiffUtil

DiffUtil

This utility class is based on a paper by Eugene W. Myers, published in the journal “Algorithmica” in November 1986

An O(Nd) difference Algorithm and Its Variations

This is an algorithm with a time complexity of O(N + D^2), where D is the size of two data directly modified,

In the actual data modification, D should not be large, so this efficiency is very high.

(Myers difference algorithm does not implement the operation of moving elements, so it uses O(N^2) time to find moving elements)

how to use

The code in the example we gave above can be modified like this

//Observe data in the View layer
 mViewModel.observeData().observe(activity, Observer { data->
            val oldList = adapter.mList ?: listOf()
            val newList = data
            adapter. submitList(newList)
            DiffUtil.calculateDiff(object : DiffUtil.Callback() {
                override fun getOldListSize() = oldList.size

                override fun getNewListSize() = newList.size

                override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                    // Regardless of case, if equals is the same element
                    return oldList[oldItemPosition].equals(newList[newItemPosition],true)
                }

                override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
                   //If equals, the content is the same
                    return oldList[oldItemPosition] == newList[newItemPosition]
                }
            }, false). dispatchUpdatesTo(adapter)
 })

//Write this in Adapter
var mList: List<String>? = null
fun submitList(list: List<String>) {
    mList = list
}

code analysis

First look at the function calculateDiff

/**
 *
 * @param cb This callback mainly provides some functions to allow the scoring algorithm to judge, and then the comparison result can be obtained
 * @param detectMoves Do you need to detect the movement of the data source? As mentioned above, if you need to detect, it will take an additional O(N^2) time, and N is the sum of deleting and adding elements
 *
 * @return The result of data comparison, we can use this result to update the UI
 */
public static DiffResult calculateDiff(@NonNull Callback cb, boolean detectMoves)

Then look at the first entry

public abstract static class Callback {
    /**
     * @return the size of the original data
     */
    public abstract int getOldListSize();

    /**
     * @return the size of the new data
     */
    public abstract int getNewListSize();

    /**
     * @param oldItemPosition original data index
     * @param newItemPosition new data index
     * @return Whether the original data and the new data are the same element, usually we think that the ID is equal to the same element
     */
    public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);

    /**
     * @param oldItemPosition original data index
     * @param newItemPosition new data index
     * @return Whether the same element has the same content, if the content is different, it will be refreshed, usually we think that the same element value (equals) means the same content,
     * Of course, this equals needs to be implemented by yourself
     */
    public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);

}
Copy Code

Then see what the return value DiffResult of calculateDiff can do

Looking at the source code, you can find that various data after the Myers differential algorithm are stored in it.

Usually we only need to use these two functions encapsulated inside

 //1
 public void dispatchUpdatesTo(@NonNull final RecyclerView.Adapter adapter) {
     dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
 }
 //2
 public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) {

//1 is to automatically call various notify methods of the adapter after parsing the verification results

//2 is to call back the parsed verification result, generally we can use //1

Careful students will find that all our operations are in the main thread, and the Myers difference algorithm uses O(N + D^2) to determine the movement of elements O(N^2)

If there are many matching elements, one hundred or one thousand, wouldn’t it affect the fluency of the application

yes it will

We can see the efficiency of this matching by looking at the DiffUtil annotation

  • 100 items and 10 modifications: avg: 0.39 ms, median: 0.35 ms
  • 100 items and 100 modifications: 3.82 ms, median: 3.75 ms
  • 100 items and 100 modifications without moves: 2.09 ms, median: 2.06 ms
  • 1000 items and 50 modifications: avg: 4.67 ms, median: 4.59 ms
  • 1000 items and 50 modifications without moves: avg: 3.59 ms, median: 3.50 ms
  • 1000 items and 200 modifications: 27.07 ms, median: 26.92 ms
  • 1000 items and 200 modifications without moves: 13.54 ms, median: 13.36 ms

1000 elements, 200 modifications, plus the case of movement, the time-consuming is 27.07ms, which is almost two frames. On the current 120 Hz mobile phones, more frame rates may be dropped

(However, the test machine is the Nexus 5X released 7 years ago, and the current machines should be much better than this (manual dog head)

So do we need to move this comparison to the child thread?

Eh! Wait, you don’t have to, Google Dad has packaged a ListAdapter for us, which maintains the thread pool and will move the view modification operation to the main thread for us, so that we can use it very conveniently DiffUtil

ReyclerView. ListAdapter

Let’s see how to use it first

Take out the code just now and change it again

//Observe data in the View layer
mViewModel.observeData().observe(activity, Observer { data->
    adapter. submitList(data)
})

//Delete this Adapter
var mList: List<String>? = null
fun submitList(list: List<String>) {
    mList = list
}

// Change the original inheritance from RecyclerView.Adapter<VH> to ListAdapter<T, VH> and pass in a DiffUtil.ItemCallback<T> to ListAdapter
// T is the data type of the list, here we are String. Can actually be a Model or data class
class MyListAdapter : ListAdapter<String, RecyclerView. ViewHolder>(object : DiffUtil. ItemCallback<String>() {
                                                                                    
    override fun areItemsTheSame(oldItem: String, newItem: String): Boolean {
        return oldItem. equals(newItem, true)
    }
                                                                                                             
    override fun areContentsTheSame(oldItem: String, newItem: String): Boolean {
        return oldItem == newItem
    }
                                                                                                             
}) {
                                                                                                             
   ··········
}

Looking at areItemsTheSame and areContentsTheSame of DiffUtil.ItemCallback is the same as what we just analyzed DiffUtil.CallBack, but this time we don’t need to look for them according to the index Elements, this time directly give us the elements, let us judge.

Source code analysis

//ListAdapter inherits from RecyclerView.Adapter which maintains an AsyncListDiffer
public abstract class ListAdapter<T, VH extends RecyclerView. ViewHolder> extends RecyclerView. Adapter<VH> {
   final AsyncListDiffer<T> mDiffer;
//AsyncListDiffer maintains an AsyncDifferConfig and a main thread executor mMainThreadExecutor
public class AsyncListDiffer<T> {
    private final ListUpdateCallback mUpdateCallback;
    @SuppressWarnings("WeakerAccess") /* synthetic access */
    final AsyncDifferConfig<T> mConfig;
    Executor mMainThreadExecutor;
//AsyncDifferConfig is passed in when ListAdapter is initialized, mBackgroundThreadExecutor is a thread pool by default, mainly for asynchronous calculation
public final class AsyncDifferConfig<T> {
    @Nullable
    private final Executor mMainThreadExecutor;
    @NonNull
    private final Executor mBackgroundThreadExecutor;
    @NonNull
    private final DiffUtil.ItemCallback<T> mDiffCallback;

Their main duties are as follows

  • ListAdapter submits data to AsyncListDiffer, calculates the difference result and then notify
  • AsyncListDiffer uses the thread pool of AsyncDifferConfig to perform differential operations based on the submitted data, and then uses the mMainThreadExecutor callback to update ListAdapter
  • AsyncDifferConfig Differential sub-thread main thread configuration, the default mBackgroundThreadExecutor is a FixedThreadPool

Let’s take a look at the overall process of ListAdapter.submitList(data)

##ListAdapter
//Give the data to AsyncListDiffer for calculation
public void submitList(@Nullable List<T> list) {
    mDiffer. submitList(list);
}

##AsyncListDiffer
//Use DiffUtil to calculate the checking result, and change the data to notify the callback set by ListAdapter
public void submitList(@Nullable final List<T> newList,
        @Nullable final Runnable commitCallback) {
        
        
   ······
   
    final List<T> oldList = mList;
    // Execute in child thread
    mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
        @Override
        public void run() {
        //Call DiffUtil.calculateDiff The method of use is the same as when we introduced DiffUtil
            final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
            
              ·······
              
            });
           // Execute on the main thread
            mMainThreadExecutor. execute(new Runnable() {
                @Override
                public void run() {
                    if (mMaxScheduledGeneration == runGeneration) {
                    //Send the result (DiffResult) to the callback set by ListAdpater
                        latchList(newList, result, commitCallback);
                    }
                }
            });
        }
    });
}


 void latchList(
         @NonNull List<T> newList,
         @NonNull DiffUtil. DiffResult diffResult,
         @Nullable Runnable commitCallback) {
         ·····
     //Are you familiar with this, as I said before, this mUpdateCallback is created when the ListAdapter is initialized
     diffResult. dispatchUpdatesTo(mUpdateCallback);
     ···
 }
                                                               

I believe that everyone has roughly understood the whole process, right? At this point, the explanation of this article is almost over.