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:
- Algorithm sounds very nb, must be a good thing;
- To simplify the
RecyclerView
Is not concerned with the callnotifyItemInserted
ornotifyItemChanged
, shall besubmitList
That’s it (thoughnotifyDataSetChanged
Can also do, but the performance of the hip pull, and no animation); LiveData
orFlow
To monitor a singleList
When it comes to data sources, it is often difficult to know the wholeList
Which data items are updated in thenotifyDataSetChanged
Method, andDiffUtil
That’s the answer to the problem, brainlesssubmitList
And 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 first
newList
With internally heldmList
Is the reference to the same, if the same, return directly; - If it’s a different quote, it’s right
newList
和mList
做Diff
Algorithm comparison, and generate comparison resultsDiffUtil.DiffResult
; - At last,
latchList
Method will benewList
Assigned tomList
And willDiff
Result of the algorithmDiffUtil.DiffResult
Application 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.
- use
Serializable
, the code looks simpler, but the performance is lower; - use
Parcelable
, 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.