Our use of DiffUtil may have been skewed

The front is my running account, think it looks boring, can directly jump to the essence section,.

The advantage of DiffUtil

When I first encountered DiffUtil, I had a lot of good feelings about DiffUtil, including:

  1. Algorithm sounds very nb, must be a good thing;
  2. To simplify theRecyclerViewIs not concerned with the callnotifyItemInsertedornotifyItemChanged, shall besubmitListThat’s it (thoughnotifyDataSetChangedCan also do, but the performance of the hip pull, and no animation);
  3. LiveDataorFlowTo monitor a singleListWhen it comes to data sources, it is often difficult to know the wholeListWhich data items are updated in thenotifyDataSetChangedMethod, andDiffUtilThat’s the answer to the problem, brainlesssubmitListAnd we’re done.

Sample DiffUtil code

When using DiffUtil, the code looks like this:

data class Item (
    var id: Long = 0.var data: String = ""
)

class DiffAdapter : RecyclerView.Adapter<DiffAdapter.MyViewHolder>() {
    / / AsyncListDiffer class in androidx. Recyclerview. Under the widget package
    // Here we use AsyncListDiffer as an example, using ListAdapter or directly using DiffUtil also has the latter problem
    private val differ = AsyncListDiffer<Item>(this.object : DiffUtil.ItemCallback<Item>() {
        override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
            // The == operator in Kotlin is equivalent to calling equals in Java
            return oldItem == newItem
        }

        private val payloadResult = Any()
        override fun getChangePayload(oldItem: Item, newItem: Item): Any {
            // Payload Is used when the local field of the Item is updated
            // This method is called when updates to the same Item are detected
            // This method returns null by default, which triggers the update animation of the Item, representing a flash of the Item
            // If the return value is not null, you can turn off the update animation for Item
            return payloadResult
        }
    })

    class MyViewHolder(val view: View):RecyclerView.ViewHolder(view){
        private val dataTv:TextView by lazy{ view.findViewById(R.id.dataTv) }
        fun bind(item: Item){
            dataTv.text = item.data}}override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
        return MyViewHolder(LayoutInflater.from(parent.context).inflate(
            R.layout.item_xxx,
            parent,
            false))}override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
        holder.bind(differ.currentList[position])
    }

    override fun getItemCount(a): Int {
        return differ.currentList.size
    }
    
    public fun submitList(newList: List<Item>) {
        differ.submitList(newList)
    }
}

val dataList = mutableListOf<Item>(item1, item2)
differ.submitList(dataList)
Copy the code

The key to the above code is the implementation of the following two methods, respectively:

  • areItemsTheSame(oldItem: Item, newItem: Item): Compares whether two items represent the same Item data;
  • areContentsTheSame(oldItem: Item, newItem: Item): Compares whether the data of two items are the same.

DiffUtil pit stepping process

The sample code above looks simple and easy to understand when we try to add a piece of data:

val dataList = mutableListOf<Item>(item1, item2)
differ.submitList(dataList)

// Add data
dataList.add(item3)
differ.submitList(dataList)
Copy the code

Item3 is not displayed in the interface. Let’s look at the AsyncListDiffer key code implementation:

public void submitList(@Nullable final List<T> newList) {
    submitList(newList, null);
}

public void submitList(@Nullable final List<T> newList, @Nullable final Runnable commitCallback) {
    / /... Omit irrelevant code
    // Notice that this is Java code comparing newList and mList to see if they are the same reference
    if (newList == mList) {
        // nothing to do (Note - still had to inc generation, since may have ongoing work)
        if(commitCallback ! =null) {
            commitCallback.run();
        }
        return;
    }
    / /... Omit irrelevant code

    mConfig.getBackgroundThreadExecutor().execute(new Runnable() {
        @Override
        public void run(a) {
            final DiffUtil.DiffResult result = DiffUtil.calculateDiff(new DiffUtil.Callback() {
                / /... Omit the Diff algorithm comparison code for newList and mList
            });

            / /... Omit irrelevant code
            mMainThreadExecutor.execute(new Runnable() {
                @Override
                public void run(a) {
                    if(mMaxScheduledGeneration == runGeneration) { latchList(newList, result, commitCallback); }}}); }}); }void latchList(
        @NonNull List<T> newList,
        @NonNull DiffUtil.DiffResult diffResult,
        @Nullable Runnable commitCallback) {
    / /... Omit irrelevant code
    mList = newList;
    // Update the result to a specific
    diffResult.dispatchUpdatesTo(mUpdateCallback);
    / /... Omit irrelevant code
}
Copy the code

SubmitList (); submitList (); submitList ();

  • Check the new submission firstnewListWith internally heldmListIs the reference to the same, if the same, return directly;
  • If it’s a different quote, it’s rightnewListmListDiffAlgorithm comparison, and generate comparison resultsDiffUtil.DiffResult;
  • At last,latchListMethod will benewListAssigned tomListAnd willDiffResult of the algorithmDiffUtil.DiffResultApplication tomUpdateCallback.

MUpdateCallback is a recyclerView. Adapter object that was passed in to create AsyncListDiffer objects.

Shallow copy

After analyzing the code, we can see that each time we submitList, we must pass in a different List object. Otherwise, the method will not do Diff algorithm comparison inside, but will return directly, and the interface will not refresh. You need a different List, right? Why not just create a new List?

So let’s modify the submitList method:

class DiffAdapter : RecyclerView.Adapter<DiffAdapter.MyViewHolder>() {
    private val differ = AsyncListDiffer<Item>(this.object : DiffUtil.ItemCallback<Item>() {
        override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem == newItem
        }
    })

    / /... Omit irrelevant code

    public fun submitList(newList: List<Item>) {
        // Create a new List and call submitList
        differ.submitList(newList.toList())
    }
}
Copy the code

The corresponding test code becomes:

val diffAdapter = ...
val dataList = mutableListOf<Item>(item1, item2)
diffAdapter.submitList(dataList)

// Add data
dataList.add(item3)
diffAdapter.submitList(dataList)

// Delete data
dataList.removeAt(0)
diffAdapter.submitList(dataList)

// Update data
dataList[1].data = "The latest data."
diffAdapter.submitList(dataList)
Copy the code

After running the code, it is found that the test code of “add data” and “delete data” is normal, but the interface does not respond when “update data” is run alone.

When we call the differ. SubmitList (newlist.tolist ()) method, we do make a copy of the List, but it is a shallow copy. AreContentsTheSame oldItem and newItem are referenced by the same object.

The copy of the data class

Some students might say: “When you update the data field, you should call copy method, copy a new object, update the new value, and then replace the original Item!” .

Here is the code:

val diffAdapter = ...
val dataList = mutableListOf<Item>(item1, item2)
diffAdapter.submitList(dataList)

// Update data
dataList[0] = dataList[0].copy(data = "The latest data.")
diffAdapter.submitList(dataList)
Copy the code

After running the code, “update data” becomes normal, and when the business is simpler, that’s the end of the story; there are no new pits to step on. But if the business is more complex, the code to update the data might look like this:

data class InnerItem(
    val innerData: String = ""
)
data class Item(
    val id: Long = 0.val data: String = "".val innerItem: InnerItem = InnerItem()
)

val diffAdapter = ...
val dataList = mutableListOf<Item>(item1, item2)
diffAdapter.submitList(dataList)

// Update data
val item = dataList[0]
// The internal data cannot be directly assigned, so you need to copy it
val innerNewItem = item.innerItem.copy(innerData = "The latest internal data.")
dataList[0] = item.copy(innerItem = innerNewItem)
diffAdapter.submitList(dataList)
Copy the code

It looks a little bit complicated, but what if we nested it a little bit deeper? I have a List nested inside, right? You have to recursively copy, and the code seems a little more complicated.

At this point, we recall the second advantage of DiffUtil mentioned at the beginning of the DiffUtil question. Instead of making the code simpler, I might as well assign it manually and call notifyItemXxx myself.

Deep copy

Thus, in order to avoid recursive copy, the code to update the data becomes too complex, so the deep-copy scheme is introduced. I care how many layers you set, I first deep copy, I directly modify the deep copy of the data, and then directly set back, the code is as follows:

data class InnerItem(
    var innerData: String = ""
)
data class Item(
    val id: Long = 0.var data: String = "".var innerItem: InnerItem = InnerItem()
)


val diffAdapter = ...
val dataList = mutableListOf<Item>(item1, item2)
diffAdapter.submitList(dataList)

// Update data
dataList[0] = dataList[0].deepCopy().apply { 
    innerItem.innerData = "The latest internal data."
}
diffAdapter.submitList(dataList)

Copy the code

The code looks much cleaner again, and our focus is on how deep copy is implemented:

Serializable or Parcelable can be used to implement deep copy of objects, specific search.

  • useSerializable, the code looks simpler, but the performance is lower;
  • useParcelable, good performance, but need to generate more extra code, looks less concise;

In fact, it doesn’t matter whether you choose Serializable or Parcelable, depending on your personal preference. The key is that after the Serializable or Parcelable interface is implemented, the data types in Item will be limited. All direct and indirect fields in Item must also implement the Serializable Parcelable interface. Otherwise, serialization will fail.

Item, for example, cannot declare types for android. In the text. The SpannableString field (used to display the rich text), because SpannableString neither implement the Serializable interface, The Parcelable interface is not implemented.

nature

Returning to DiffUtil, let’s look again at the two core methods:

  • areItemsTheSame(oldItem: Item, newItem: Item): Compares whether two items represent the same Item data;
  • areContentsTheSame(oldItem: Item, newItem: Item): Compares whether the data of two items are the same.

Let me ask you a question, what are these two methods for, or what are their roles in the algorithm?

Simple, even if I do not know the DiffUtil algorithm implementation (in fact, I do not know O (~ ▽ ~) O), you can guess that only with areItemsTheSame method we can implement the following three operations:

  • itemRemove
  • itemInsert
  • itemMove

The last itemChange operation requires the areItemsTheSame method to return true and the areContentsTheSame method to return false. This also corresponds to the annotation for this method:

This method is called only if {@link #areItemsTheSame(T, T)} returns {@code true} for these items.
Copy the code

Therefore, the areContentsTheSame method is only used to determine whether the items used for display have been updated. It is not necessary to call equals to fully compare all the fields of the two items. The areContentsTheSame method is also described in the code comment:

This method to check equality instead of {@link Object#equals(Object)} so that you can change its behavior depending on your UI.
For example, if you are using DiffUtil with a {@link RecyclerView.Adapter RecyclerView.Adapter}, you should return whether the items' visual representations are the same.
Copy the code

You’ll find that many examples of code on the web use equals to determine whether data has been modified, and then update data based on equals, both recursive copy and deep copy, which is biased and restricted.

Ways to improve

Since the areItemsTheSame method is only used to determine whether the part of the Item that is displayed has been updated, thus determining the itemChange operation, we can create a new contentId field to indicate the uniqueness of the content. The implementation of the areItemsTheSame method also only compares contentId, and the code looks like this:

private val contentIdCreator = AtomicLong()
abstract class BaseItem(
    open val id: Long.val contentId: Long = contentIdCreator.incrementAndGet()
)
data class InnerItem(
    var innerData: String = ""
)
data class ItemImpl(
    override val id: Long.var data: String = "".var innerItem: InnerItem = InnerItem()
) : BaseItem(id)

class DiffAdapter : RecyclerView.Adapter<DiffAdapter.MyViewHolder>() {
    private val differ = AsyncListDiffer<Item>(this.object : DiffUtil.ItemCallback<Item>() {
        override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
            // If the contentId is inconsistent, the Item data is considered updated
            return oldItem.contentId == newItem.contentId
        }
        
        private val payloadResult = Any()
        override fun getChangePayload(oldItem: Item, newItem: Item): Any {
            // Payload Is used when the local field of the Item is updated
            // This method is called when updates to the same Item are detected
            // This method returns null by default, which triggers the update animation of the Item, representing a flash of the Item
            // If the return value is not null, you can turn off the update animation for Item
            return payloadResult
        }
    })

    / /... Omit irrelevant code

    public fun submitList(newList: List<Item>) {
        // Create a shallow copy of the new List and call submitList
        differ.submitList(newList.toList())
    }
}


val diffAdapter: DiffAdapter = ...
val dataList = mutableListOf<Item>(item1, item2)
diffAdapter.submitList(dataList)

// Update data
// Just copy the outer data and ensure that the contentId is inconsistent
// Since ItemImpl inherits from BaseItem, when the itemImp.copy method is executed, the BaseItem constructor is called to generate a new contentId
dataList[0] = dataList[0].copy().apply { 
    data = "The latest data."
    innerItem.innerData = "The latest internal data."
}
diffAdapter.submitList(dataList)

Copy the code

Because the areContentsTheSame method requires two different objects to be compared, the copy method is used to generate a new object when a field is updated.

The areContentsTheSame method should return false in this case, because some of the ItemImpl fields may be updated without affecting the display. However, in my opinion, such cases are rare and misjudgment is acceptable. The cost is only an additional update of interface item.

In fact, once we understand the essence, we can customize it in our own way according to our business needs. What about Java, for example? Here’s what we might do:

class Item{
    int id;
    boolean isUpdate; // This field is used to mark whether the Item has been updated
    String data;
}

class JavaDiffAdapter extends RecyclerView.Adapter<JavaDiffAdapter.MyViewHolder>{
    public void submitList(List<Item> dataList){
        differ.submitList(new ArrayList<>(dataList));
    }
    @NonNull
    @Override
    public MyViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        return new MyViewHolder(LayoutInflater.from(parent.getContext()).inflate(
                R.layout.item_xxx, 
                parent,
                false
        ));
    }
    @Override
    public void onBindViewHolder(@NonNull MyViewHolder holder, int position) {
        // After binding data once, assign false to the identity that needs to be updated
        differ.getCurrentList().get(position).isUpdate = false;
        holder.bind(differ.getCurrentList().get(position));
    }
    @Override
    public int getItemCount(a) {
        return differ.getCurrentList().size();
    }
    class MyViewHolder extends RecyclerView.ViewHolder{
        private TextView dataTv;
        public MyViewHolder(View itemView) {
            super(itemView);
            dataTv = itemView.findViewById(R.id.dataTv);
        }
        private void bind(Item item){ dataTv.setText(item.data); }}private AsyncListDiffer<Item> differ = new AsyncListDiffer<Item>(this.new DiffUtil.ItemCallback<Item>() {
        @Override
        public boolean areItemsTheSame(@NonNull Item oldItem, @NonNull Item newItem) {
            return oldItem.id == newItem.id;
        }
        @Override
        public boolean areContentsTheSame(@NonNull Item oldItem, @NonNull Item newItem) {
            // Check whether the data is updated by reading isUpdate
            return! newItem.isUpdate; }private final Object payloadResult = new Object();
        @Nullable
        @Override
        public Object getChangePayload(@NonNull Item oldItem, @NonNull Item newItem) {
            // Payload Is used when the local field of the Item is updated
            // This method is called when updates to the same Item are detected
            // This method returns null by default, which triggers the update animation of the Item, representing a flash of the Item
            // If the return value is not null, you can turn off the update animation for Item
            returnpayloadResult; }}); }// Update dataList<Item> dataList = ... ; Item target = dataList.get(0);
// Indicates that the data is updated
target.isUpdate = true;
target.data = "New data";
adapter.submitList(dataList);
Copy the code

The last

If your List

comes from Room, you can call submitList, and you don’t have to worry about it, because when Room data is updated, it automatically generates a new List

. Each Item is also new. For code examples, refer to the AsyncListDiffer or ListAdapter class comment at the top. Be aware that too many temporary objects are generated when data is updated too frequently.