Pain points and problems
RecyclerView has gradually become a necessary control for Android development to write a sliding layout, but most of the project used is notifyDataSetChanged, and in method annotation is actually more recommended that we directly use the four methods of add, delete, change.
/** * Notify any registered observers that the data set has changed. * * <p>There are two different classes of data change events, item changes and structural * changes. Item changes are when a single item has its data updated but no positional * changes have occurred. Structural changes are when items are inserted, removed or moved * within the data set.</p> * * <p>This event does not specify what about the data set has changed, forcing * any observers to assume that all existing items and structure may no longer be valid. * LayoutManagers will be forced to fully rebind and relayout all visible views.</p> * * <p><code>RecyclerView</code> will attempt to synthesize visible structural change events * for adapters that report that they have {@link #hasStableIds() stable IDs} when
* this method is used. This can help for the purposes of animation and visual
* object persistence but individual item views will still need to be rebound
* and relaid out.</p>
*
* <p>If you are writing an adapter it will always be more efficient to use the more
* specific change events if you can. Rely on <code>notifyDataSetChanged()</code>
* as a last resort.</p>
*
* @see #notifyItemChanged(int)
* @see #notifyItemInserted(int)
* @see #notifyItemRemoved(int)
* @see #notifyItemRangeChanged(int, int)
* @see #notifyItemRangeInserted(int, int)
* @see #notifyItemRangeRemoved(int, int)
*/
public final void notifyDataSetChanged(a) {
mObservable.notifyChanged();
}
Copy the code
But in real development, it might be simpler just to add pages, and we can use notifyItemRangeInserted to do the insert. But when data structures are added, deleted, replaced, etc., things get complicated. Google also considers this problem, and it is not friendly to let developers directly make judgment of data content changes, so the DiffUtil tool is provided in the support package for us to do post-development of data changes.
The underlying level of Paging in Android AAC is also Item difference based on DiffUtil calculation, but we will not expand on Paging, for reasons that will be analyzed step by step later.
Encapsulate based on DiffUtil
I also specially went to check some relevant article content before the article starts, my personal opinion wrote is still a little subtle. Let’s talk about how it works, and then we’ll talk about some pain points.
public static DiffResult calculateDiff(@NonNull Callback cb, boolean detectMoves) {... }/** * A Callback class used by DiffUtil while calculating the diff between two lists. */
public abstract static class Callback {
/**
* Returns the size of the old list.
*
* @return The size of the old list.
*/
public abstract int getOldListSize(a);
/**
* Returns the size of the new list.
*
* @return The size of the new list.
*/
public abstract int getNewListSize(a);
/**
* Called by the DiffUtil to decide whether two object represent the same Item.
* <p>
* For example, if your items have unique ids, this method should check their id equality.
*
* @param oldItemPosition The position of the item in the old list
* @param newItemPosition The position of the item in the new list
* @return True if the two items represent the same object or false if they are different.
*/
public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition);
/**
* Called by the DiffUtil when it wants to check whether two items have the same data.
* DiffUtil uses this information to detect if the contents of an item has changed.
* <p>
* DiffUtil uses 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.
* <p>
* This method is called only if {@link #areItemsTheSame(int, int)} returns
* {@code true} for these items.
*
* @param oldItemPosition The position of the item in the old list
* @param newItemPosition The position of the item in the new list which replaces the
* oldItem
* @return True if the contents of the items are the same or false if they are different.
*/
public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition);
}
Copy the code
From the source analysis, the Callback comment makes it clear that DiffUtil compares two List structures. DiffUtil uses the interface of the CallBack (just a brief introduction, the process is more complicated) to first compare the sizes of the two lists and then compare each element to see if it is the same Item. If it is the same Item, the DiffUtil compares the elements of the same Item. Then generate a DiffResult result and develop subsequent operations such as adding, deleting and changing according to the result.
AreContentsTheSame is used to compare elements that are identical. Normally, the Id defined inside the model is used as the unique identifier of the model, and this identifier is used to determine whether the elements are identical or not.
The method to compare the contents of an element to whether they are identical is areItemsTheSame. I directly use the equals method of the Object to determine whether the contents are identical.
Pain points and problems
The first thing we need is two lists, one for old data (OldList) and one for changed data (NewList). Then the two lists are compared to make the difference. The contents of the Adapter are then refreshed based on the difference results.
NotifyDataSetChanged (); notifyDataSetChanged (); notifyDataSetChanged The way many bloggers do this is to create a new array and put the metadata into the new array, so my VM or presenter can just manipulate the metadata, so when the data changes I call the refresh method and then let DiffUtil do the data difference.
It seems that we have solved the problem of generating two lists. However, since this copy is only a shallow copy, the element is still compared to itself when areItemsTheSame is performed. Therefore, shallow copies cannot use DiffUtil perfectly.
Deep copy
A deep copy is a perfect copy of an identical but independent target object from the source object.
The internal DiffUtil of Paging generates a snapshot ListSnapshotPagedList when a List is passed in. The snapshot is OldList and the OldList is compared with the snapshot version. The OldList of the snapshot is a deep copy of the metadata. Both the snapshot and the source data are retrieved from the DataSource.
Pacel does data replication
If you are familiar with cross-process communication, you should know that Parcelable’s type of content is the fastest copy speed in transmission. So how does Parcelable copy data?
Pacel is responsible for copying Parcelable data. When cross-processes are not involved, Pacel creates a separate area in memory for Parcelable data content, which can then be copied using a Parcel.
valparcelable = itemsCursor? .get(oldPosition) as Parcelable
val parcel = Parcel.obtain()
parcelable.writeToParcel(parcel, 0)
parcel.setDataPosition(0)
val constructor = parcelable.javaClass.getDeclaredConstructor(Parcel::class.java)
constructor.isAccessible = true
val dateEntity = constructor.newInstance(parcel) asT mData? .let { it.add(oldPosition, dateEntity) } parcel.recycle()Copy the code
The above is the operation I used to do data copy. Simply construct a Parcel.obtain() object and then call the writeToParcel method of the source data to pass the Parcel into the metadata for an in-memory paste operation. Each object that implements the Parcelable interface has a constructor that contains a Parcel, which we call reflectively to generate a new copy object.
Project analysis
Project github repository address
Paging replaces all adapters and passes in a Diff Callback in order to use Paging. But who doesn’t package a BaseAdapter? There may be operations like header and footer in it. If you change the inheritance and pass in an argument, maybe your colleague will jump up and hit you in the knee.
In my opinion, if DiffUtil can be used in combination with the current Adapter, then the transformation cost will be relatively low. As long as we can copy the data inside DiffUtil, compare the data and inform the adapter of changes, I can decide which can be upgraded to Diff first and which can not be changed according to my needs.
First of all, Diff is comparing data models, so can we enhance the model layer to abstract and adapt the unique values and Equals method?
So the core of the repository is only two, the first is composition, and the second is enhancement of the model layer.
DiffHelper combined class
class DiffHelper<T> {
private var itemsCursor: MutableList<T>? = null
private var mData: CopyOnWriteArrayList<T>? = null
var diffDetectMoves = true
var callBack: ListUpdateCallback? = null
private val mMainThreadExecutor: Executor = MainThreadExecutor()
private val mBackgroundThreadExecutor: Executor = Executors.newFixedThreadPool(2)
private class MainThreadExecutor internal constructor() : Executor {
val mHandler = Handler(Looper.getMainLooper())
override fun execute(command: Runnable) {
mHandler.post(command)
}
}
fun setData(itemsCursor: MutableList<T>? , ignore:Boolean = false) {
this.itemsCursor = itemsCursor itemsCursor? .apply { mBackgroundThreadExecutor.execute {if (mData == null) {
copyData()
}
if(! ignore) { mMainThreadExecutor.execute { callBack? .onInserted(0, itemsCursor.size)
}
}
}
}
}
private fun copyData(a) {
try{ itemsCursor? .apply {if (isNotEmpty()) {
if (this[0] is Parcelable) {
mData = CopyOnWriteArrayList()
} else {
mData = CopyOnWriteArrayList()
for (entity in this) { mData? .add(entity) }return}}else {
mData = CopyOnWriteArrayList()
return
}
for (entity in this) {
val parcel = Parcel.obtain()
(entity as Parcelable).writeToParcel(parcel, 0)
parcel.setDataPosition(0)
val constructor = entity.javaClass.getDeclaredConstructor(Parcel::class.java)
constructor.isAccessible = true
val dateEntity = constructor.newInstance(parcel) asT mData? .add(dateEntity) parcel.recycle() } } }catch (e: Exception) {
e.printStackTrace()
}
}
fun notifyItemChanged(a) {
mBackgroundThreadExecutor.execute {
val diffResult = diffUtils()
mMainThreadExecutor.execute {
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
override fun onInserted(position: Int, count: Int){ callBack? .onInserted(position, count) }override fun onRemoved(position: Int, count: Int){ callBack? .onRemoved(position, count) }override fun onMoved(fromPosition: Int, toPosition: Int){ callBack? .onMoved(fromPosition, toPosition) }override fun onChanged(position: Int, count: Int, payload: Any?).{ callBack? .onChanged(position, count, payload) } }) } } }@Synchronized
private fun diffUtils(a): DiffUtil.DiffResult {
val diffResult =
DiffUtil.calculateDiff(BaseDiffCallBack(mData, itemsCursor), diffDetectMoves)
copyData()
return diffResult
}
fun getItemSize(a): Int {
returnitemsCursor? .size ? :0
}
fun <T> getEntity(pos: Int): T? {
return if(itemsCursor? .size ? :0 <= pos || pos < 0) null elseitemsCursor? .get(pos) as T
}
}
Copy the code
I first abstracts a proxy method, data source content, provides four methods, setData, notifyItemChanged, getItemSize, getEntity these four methods.
- SetData Sets the data source method, and performs the first data copy operation when setting the data source.
- NotifyItemChanged This method calls DiffUtil directly. When the content of the data source changes, this method notifies the Adapter of the change by way of interface backout.
- GetItemSize Adapter gets the length of the current data source, replacing the size method inside the adapter.
- GetEntity gets the data entity type.
Abstract unified Model
class BaseDiffCallBack(private val oldData: List< * >? .private val newData: List< * >? :DiffUtil.Callback(a){
override fun getOldListSize(a): Int {
returnoldData? .size ? :0
}
override fun getNewListSize(a): Int {
returnnewData? .size ? :0
}
override fun areItemsTheSame(p0: Int, p1: Int): Boolean { val object1 = oldData? .get(p0) val object2 = newData? .get(p1)if (object1 == null || object2 == null)
return false
return if (object1 is IDifference && object2 is IDifference) {
TextUtils.equals(object1.uniqueId, object2.uniqueId)
} else {
object1 == object2
}
}
override fun areContentsTheSame(p0: Int, p1: Int): Boolean { val object1 = oldData? .get(p0) val object2 = newData? .get(p1)return if (object1 is IEqualsAdapter && object2 is IEqualsAdapter) {
object1 == object2
} else {
true}}}Copy the code
This class is compared to the abstract model layer.
areItemsTheSame
Method compares whether the model layer is implementedIDifference
To perform a unique value comparison by comparing the interface.areContentsTheSame
Is based on whether the current model layer is implementedIEqualsAdapter
, indicates that the value content does not need to be compared if it is not implemented, and directly compares the value content if it is.
TODO
In fact, the time of a Diff call is relatively time-consuming, I did not work on this part, it is reasonable to use async to implement. We can optimize that.
AsyncListDiffer (1) AsyncListDiffer (1) AsyncListDiffer
How to use
The definition of the data model must first implement Parcelable(deep-copy logic), and then must implement the IDifference interface, mainly to tell whether the data body has changed.
(Optional) After the Interface is implemented, the IEqualsAdapter notifies the Adapter of data changes. We can use the IDEA plugin or KT data to implement the equals method of the model, to do the same element content comparison, after all, equals method is still disgusting to write.
data class TestEntity(var id: Int = 0.var displayTime: Long = 0.var text: String? = Random().nextInt(10000).toString()) : Parcelable, IDifference, IEqualsAdapter {
override val uniqueId: String
get() = id.toString()
fun update(a) {
displayTime = System.currentTimeMillis()
text = "Update data"
}
constructor(source: Parcel) : this(
source.readInt(),
source.readLong(),
source.readString()
)
override fun describeContents(a) = 0
override fun writeToParcel(dest: Parcel, flags: Int) = with(dest) {
writeInt(id)
writeLong(displayTime)
writeString(text)
}
companion object {
@JvmField
val CREATOR: Parcelable.Creator<TestEntity> = object : Parcelable.Creator<TestEntity> {
override fun createFromParcel(source: Parcel): TestEntity = TestEntity(source)
override fun newArray(size: Int): Array<TestEntity? > = arrayOfNulls(size) } } }Copy the code
Initialize and pass in the data, and set the data to flush back, if you have headers or whatever.
val diffHelper: DiffHelper<Any> = DiffHelper()
diffHelper.callBack = SimpleAdapterCallBack(this)
diffHelper.setData(items)
Copy the code
When the list changes (adding, deleting, or modifying anything), a data refresh is called.
diffHelper.notifyItemChanged()
Copy the code
finished
I’m going to end by telling you a story about a eunuch