It’s a luxury to refresh the entire list with each data change, not only does the entire list blink, but all visible entries redraw the list by executing onBindViewHolder() again (even if it doesn’t need to refresh). If the entry view is complex, the performance of the list will be significantly affected.

A more efficient refresh method is to refresh only the entries whose data has changed. RecyclerView.Adapter has 4 incomplete refresh methods, respectively: NotifyItemRangeInserted (), notifyItemRangeChanged(), notifyItemRangeRemoved, notifyItemMoved(). They all need to specify the range of change when calling, which requires the business layer to know the details of the data change, which undoubtedly increases the difficulty of calling.

DiffUtil template code

Androidx. Recyclerview. There is a utility class called DiffUtil under the widget package, it takes advantage of an algorithm to calculate the difference between the two lists, and can be directly applied to recyclerview. The Adapter, automatically refresh the whole quantity.

The template code for using DiffUtil is as follows:

val oldList = ... / / the old list
val newList = ... / / the new list
valAdapter: RecyclerView. adapter =...// 1. Define the comparison method
val callback = object : DiffUtil.Callback() {
    override fun getOldListSize(a): Int = oldList.size
    override fun getNewListSize(a): Int = newList.size
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    	// Get the elements at the corresponding positions in the old and new lists
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]
        return.// Define when old and new elements are the same object (usually a business ID)
    }
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]
        return.// Define when the contents of the same object are the same (as determined by business logic)
    }
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]
        return.// Specify how the contents of the same object differ (the return value is passed as payloads onBindViewHoder())}}// 2. Compare and output the result
val diffResult = DiffUtil.calculateDiff(callback)
// 3. Apply the comparison results to the Adapter
diffResult.dispatchUpdatesTo(adapter)
Copy the code

DiffUtil takes three inputs, an old list, a new list, and a DiffUtil.Callback, whose implementation is related to the business logic that defines how to compare the data in the list.

Determining whether the data in the list are the same is divided into three progressive levels:

  1. Check whether the data is the sameareItemsTheSame()
  2. If the data is the same, are the specific contents the sameareContentsTheSame()(whenareItemsTheSame()Will only be called if it returns true.
  3. If the specific content of the same data is different, then find out the difference: correspondinggetChangePayload()(whenareContentsTheSame()Will only be called if false is returned.

The DiffUtil output 1 comparison result, which can be applied to recyclerView. Adapter:

// Apply the comparison results to the Adapter
public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
    dispatchUpdatesTo(new AdapterListUpdateCallback(adapter));
}

// Apply the comparison results to ListUpdateCallback
public void dispatchUpdatesTo(@NonNull ListUpdateCallback updateCallback) {... }// List update callback based on recyclerView. Adapter
public final class AdapterListUpdateCallback implements ListUpdateCallback {
    private final RecyclerView.Adapter mAdapter;
    public AdapterListUpdateCallback(@NonNull RecyclerView.Adapter adapter) {
        mAdapter = adapter;
    }
    @Override
    public void onInserted(int position, int count) {
    	// Interval insertion
        mAdapter.notifyItemRangeInserted(position, count);
    }
    @Override
    public void onRemoved(int position, int count) {
    	// Range removed
        mAdapter.notifyItemRangeRemoved(position, count);
    }
    @Override
    public void onMoved(int fromPosition, int toPosition) {
    	/ / move
        mAdapter.notifyItemMoved(fromPosition, toPosition);
    }
    @Override
    public void onChanged(int position, int count, Object payload) {
    	// Interval updatemAdapter.notifyItemRangeChanged(position, count, payload); }}Copy the code

DiffUtil feeds the comparison results back to the business layer in the form of a ListUpdateCallback callback. Insert, remove, move and update these four callbacks represent the four possible changes of the list content. For RecyclerView.Adapter, it just corresponds to four incomplete update methods.

Diffutil. Callback is decoupled from the service

Different business scenarios require different diffUtil.callback implementations because it is coupled to specific business data. This prevents it from being used with the type-independent adapters described in the previous article.

Is there any way to makeDiffUtil.CallbackIs the implementation decoupled from the concrete business data?

Here the business logic is “compare data is consistent” algorithm, can not write this logic in the data class body?

Proposed a new interface:

interface Diff {
    // Check whether the current object and the given object are the same object
    fun isSameObject(other: Any): Boolean
    // Determine whether the current object and the given object have the same content
    fun hasSameContent(other: Any): Boolean
    // Returns the difference between the current object and the given object
    fun diff(other: Any): Any
}
Copy the code

Then let the data class implement the interface:

data class Text(
    var text: String,
    var type: Int.var id: Int
) : Diff {
    override fun isSameObject(other: Any): Boolean = this.id == other.id
    override fun hasSameContent(other: Any): Boolean = this.text == other.text
    override fun diff(other: Any?).: Any? {
        return when {
            other !is Text -> null
            this.text ! = other.text -> {"text change"}
            else -> null}}}Copy the code

This allows the diffUtil.callback logic to be decoupled from the business data:

// Contains a list of any data types
val newList: List<Any> = ... 
val oldList: List<Any> = ...
val callback = object : DiffUtil.Callback() {
    override fun getOldListSize(a): Int = oldList.size
    override fun getNewListSize(a): Int = newList.size
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
    	// Convert data to Diff
        val oldItem = oldList[oldItemPosition] as? Diff
        val newItem = newList[newItemPosition] as? Diff
        if (oldItem == null || newItem == null) return false
        return oldItem.isSameObject(newItem)
    }
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldList[oldItemPosition] as? Diff
        val newItem = newList[newItemPosition] as? Diff
        f (oldItem == null || newItem == null) return false
        return oldItem.hasSameContent(newItem)
    }
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        val oldItem = oldList[oldItemPosition] as? Diff
        val newItem = newList[newItemPosition] as? Diff
        if (oldItem == null || newItem == null) return null
        return oldItem.diff(newItem)
    }
}
Copy the code

On second thought, the base class Any for all non-empty classes contains these semantics:

public open class Any {
    // Determine if the current object and another object are the same object
    public open operator fun equals(other: Any?).: Boolean
    // Returns the current object hash value
    public open fun hashCode(a): Int
}
Copy the code

This simplifies the Diff interface:

interface Diff {
    infix fun diff(other: Any?).: Any?
}
Copy the code

The reserved word infix indicates that this function can be called using infix expressions to increase code readability (see the following code), which can be explained here.

The implementation of the data entity class and DiffUtil.callback is also simplified:

data class Text(
    var text: String,
    var type: Int.var id: Int
) : Diff {
    override fun hashCode(a): Int = this.id
    override fun diff(other: Any?).: Any? {
        return when {
            other !is Text -> null
            this.text ! = other.text -> {"text diff"}
            else -> null}}override fun equals(other: Any?).: Boolean {
        return (other as? Text)? .text ==this.text
    }
}

val callback = object : DiffUtil.Callback() {
    override fun getOldListSize(a): Int = oldList.size
    override fun getNewListSize(a): Int = newList.size
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]
        return oldItem.hashCode() == newItem.hashCode()
    }
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]
        return oldItem == newItem
    }
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        val oldItem = oldList[oldItemPosition] as? Diff
        val newItem = newList[newItemPosition] as? Diff
        if (oldItem == null || newItem == null) return null
        return oldItem diff newItem // infix expression}}Copy the code

Asynchronous DiffUtil. CalculateDiff ()

Comparing algorithms is time consuming, and it is safe to asynchronize them.

Androidx. Recyclerview. Already have a widget package can directly use AsyncListDiffer:

// A specific data type must be specified
public class AsyncListDiffer<T> {
    // The background thread that performs the comparison
    Executor mMainThreadExecutor;
    // Is used to throw the result of the comparison to the main thread
    private static class MainThreadExecutor implements Executor {
        final Handler mHandler = new Handler(Looper.getMainLooper());
        MainThreadExecutor() {}
        @Override
        public void execute(@NonNull Runnable command) { mHandler.post(command); }}// Submit the new list data
    public void submitList(@Nullable final List<T> newList){
    	// Execute the comparison in the background...}... }Copy the code

It performs the comparison in the background thread and throws the result to the main thread. Unfortunately, it is type-bound and cannot be used with typeless adapters.

Helpless can only refer to its idea to write a new own:

class AsyncListDiffer(
    // listUpdateCallback is used to enable AsyncListDiffer to differ beyond recyclerView. Adapter
    var listUpdateCallback: ListUpdateCallback,
    // A custom coroutine scheduler that ADAPTS to existing code and puts the alignment logic in the existing thread instead of starting a new one
    dispatcher: CoroutineDispatcher 
) : DiffUtil.Callback(), CoroutineScope by CoroutineScope(SupervisorJob() + dispatcher) {
    // Can be filled with any type of old and new list
    var oldList = listOf<Any>()
    var newList = listOf<Any>()
    // Used to mark each submission list
    private var maxSubmitGeneration: Int = 0
    // Submit the new list
    fun submitList(newList: List<Any>) {
        val submitGeneration = ++maxSubmitGeneration
        this.newList = newList
        // Quick back: there is nothing to update
        if (this.oldList == newList) return
        // Quick return: old list is empty, new list is received in full
        if (this.oldList.isEmpty()) {
            this.oldList = newList
            // Save a snapshot of the latest data in the list
            oldList = newList.toList()
            listUpdateCallback.onInserted(0, newList.size)
            return
        }
        // Start coroutine comparison data
        launch {
            val diffResult = DiffUtil.calculateDiff(this@AsyncListDiffer)
            // Save a snapshot of the latest data in the list
            oldList = newList.toList()
            // Throw the comparison result to the main thread and apply it to the ListUpdateCallback interface
            withContext(Dispatchers.Main) {
                // Only the result of the last submitted comparison is retained, all others are discarded
                if (submitGeneration == maxSubmitGeneration) {
                    diffResult.dispatchUpdatesTo(listUpdateCallback)
                }
            }
        }
    }

    override fun getOldListSize(a): Int = oldList.size
    override fun getNewListSize(a): Int = newList.size
    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]
        return oldItem.hashCode() == newItem.hashCode()
    }
    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldList[oldItemPosition]
        val newItem = newList[newItemPosition]
        return oldItem == newItem
    }
    override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
        val oldItem = oldList[oldItemPosition] as? Diff
        val newItem = newList[newItemPosition] as? Diff
        if (oldItem == null || newItem == null) return null
        return oldItem diff newItem
    }
}
Copy the code

AsyncListDiffer implements diffUtil.callback and the CoroutineScope interface, and delegates the latter implementation to the CoroutineScope(Container handling) instance. The advantage of this is that the coroutine can be started anywhere within AsyncListDiffer without any problems, while the coroutine resources can be freed externally by an instance of AsyncListDiffer calling Cancel ().

Including detailed explain about the class delegate can click Kotlin combat | 2 = 12? Generics, class, the integrated application of overloaded operators, about the detail can click on Kotlin basis of coroutines | coroutines why use?

Let the typeless adapter hold AsyncListDiffer and you are done:

class VarietyAdapter(
    private var proxyList: MutableList<Proxy<*, *>> = mutableListOf(),
    dispatcher: CoroutineDispatcher = Dispatchers.IO // The comparison is performed in the IO shared thread pool by default
) : RecyclerView.Adapter<ViewHolder>() {
    // Build the data comparator
    private val dataDiffer = AsyncListDiffer(AdapterListUpdateCallback(this), dispatcher)
    // The business code populates the data by assigning a value to dataList
    var dataList: List<Any>
        set(value) {
            // Delegate the fill data to the data comparator
            dataDiffer.submitList(value)
        }
        // Returns a snapshot of the data since the last comparison
        get() = dataDiffer.oldList
        
        override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        	dataDiffer.cancel() // Release coroutine resources when the adapter exits RecyclerView}... }Copy the code

Lists only VarietyAdapter AsyncListDiffer related section, its detailed application can click on the proxy pattern | every time for the new type RecyclerView is mad.

It can then be used like this:

var itemNumber = 1
// Build the adapter
val varietyAdapter = VarietyAdapter().apply {
    // Add two new data types to the list
    addProxy(TextProxy())
    addProxy(ImageProxy())
    // Initial data set (containing two different types of data)
    dataList = listOf(
        Text("item ${itemNumber++}"),
        Image("#00ff00"),
        Text("item ${itemNumber++}"),
        Text("item ${itemNumber++}"),
        Image("#88ff00"),
        Text("item ${itemNumber++}"))// Preload (preload the next screen when the drop-down list is pulled up)
    onPreload = {
    	// Get the old list snapshot (deep copy)
        val oldList = dataList
        // Add new content to the end of the old list snapshot
        dataList = oldList.toMutableList().apply {
            addAll(
                listOf(
                    Text("item ${itemNumber++}".2),
                    Text("item ${itemNumber++}".2),
                    Text("item ${itemNumber++}".2),
                    Text("item ${itemNumber++}".2),
                    Text("item ${itemNumber++}".2),))}}}// Apply the adapterrecyclerView? .adapter = varietyAdapter recyclerView? .layoutManager = LinearLayoutManager(this)
Copy the code

Talk is cheap, show me the code

trailer

The next chapter will introduce a super simple RecyclerView preloading method.

Recommended reading

RecyclerView series article directory is as follows:

  1. RecyclerView caching mechanism | how to reuse table?

  2. What RecyclerView caching mechanism | recycling?

  3. RecyclerView caching mechanism | recycling where?

  4. RecyclerView caching mechanism | scrap the view of life cycle

  5. Read the source code long knowledge better RecyclerView | click listener

  6. Proxy mode application | every time for the new type RecyclerView is crazy

  7. Better RecyclerView table sub control click listener

  8. More efficient refresh RecyclerView | DiffUtil secondary packaging

  9. Change an idea, super simple RecyclerView preloading

  10. RecyclerView animation principle | change the posture to see the source code (pre – layout)

  11. RecyclerView animation principle | pre – layout, post – the relationship between the layout and scrap the cache

  12. RecyclerView animation principle | how to store and use animation attribute values?

  13. RecyclerView list of interview questions | scroll, how the list items are filled or recycled?

  14. RecyclerView interview question | what item in the table below is recycled to the cache pool?

  15. RecyclerView performance optimization | to halve load time table item (a)

  16. RecyclerView performance optimization | to halve load time table item (2)

  17. RecyclerView performance optimization | to halve load time table item (3)

  18. How does RecyclerView roll? (a) | unlock reading source new posture

  19. RecyclerView how to achieve the scrolling? (2) | Fling

  20. RecyclerView Refresh list data notifyDataSetChanged() why is it expensive?