English | 简体中文 | 繁體中文 | Русский язык | Français | Español | Português | Deutsch | 日本語 | 한국어 | Italiano | بالعربية

تفسير لـ DiffUtil في الأدوات في Android 7.0

الملخص

DiffUtil هو مكتبة جديدة في support-v7:24.2.0، وهي تستخدم لتحليل مجموعتين من البيانات، لمعرفة الفرق الأدنى بين مجموعة البيانات القديمة والمجموعة الجديدة.

عندما نتحدث عن مجموعة البيانات، يجب أن تعرفوا أنها مرتبطة بشيء، وهي حبيتي، RecyclerView.

من خلال استخدامها في هذه الأيام، أرى أن أكبر استخدام لها هو عدم استخدام mAdapter.notifyDataSetChanged() عند تحديث RecyclerView.

لدينا عيبين لاستخدام mAdapter.notifyDataSetChanged() دون تفكير:

1. لن يسبب تحريك RecyclerView (حذف، إضافة، تحرك، تغيير) تأثيرًا رسوميًا.

2. أداء منخفض، لأنه يتم تحديث RecyclerView كله دون تفكير، في الحالة القصوى: إذا كانت مجموعة البيانات القديمة والجديدة متطابقة، فإن الكفاءة أقل.

بعد استخدام DiffUtil، سيصبح الكود كما يلي:

DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
diffResult.dispatchUpdatesTo(mAdapter);

سيقوم تلقائيًا بحساب الفرق بين مجموعة البيانات القديمة والمجموعة الجديدة، وفقًا للحالة الفرق، سيقوم تلقائيًا بتشغيل الأربعة طرق التالية

adapter.notifyItemRangeInserted(position, count);
adapter.notifyItemRangeRemoved(position, count);
adapter.notifyItemMoved(fromPosition, toPosition);
adapter.notifyItemRangeChanged(position, count, payload);

من الواضح أن هذه الأربع طرق تنفذ مع تأثيرات رسومية لRecyclerView، وهي طرق تحديث مخصصة، مما يرفع من كفاءة التحديث.
كالعادة، أبدأ بالشكل،

الشكل الأول يظهر تأثير استخدام mAdapter.notifyDataSetChanged() بدون تفكير، يمكن رؤية أن عملية التحديث تتم بطرق قاسية، حيث يظهر العنصر فجأة في مكان معين.

الشكل الثاني يظهر تأثير استخدام DiffUtils، وأكثر ما يميزه هو وجود تأثيرات تحرك وإدراج العناصر (Item).

تحويله إلى GIF قد يكون سيئًا بعض الشيء، ولكن تحميل ديمو في نهاية المقال سيظهر لك تأثيره بشكل أفضل.

هذا المقال سيشمل ليس فقط ما يلي، بل أيضًا ما يلي:

سأبدأ بشرح استخدام DiffUtil بسيط، لتحقيق تأثير التحديث المحدود في عملية التحديث (التحديث المحدود هو ما أسميه أنا).
استخدام DiffUtil لتحقيق تحديث جزئي عند تغيير محتوى (data) فقط للعنصر دون تغيير موقعه (position)، مما يسمى بـ Partial bind (التحديث الجزئي).
3 了解到 RecyclerView.Adapter还有public void onBindViewHolder(VH holder, int position, List<Object> payloads)方法,并掌握它。
4 在子线程中计算DiffResult,在主线程中刷新RecyclerView。
5 少部分人不喜欢的notifyItemChanged()导致Item白光一闪的动画 如何去除。
6 DiffUtil部分类、方法 官方注释的汉化

二 DiffUtil的简单用法

前文也提到,DiffUtil是帮助我们在刷新RecyclerView时,计算新老数据集的差异,并自动调用RecyclerView.Adapter的刷新方法,以完成为高效刷新并伴有Item动画的效果。

那么我们在学习它之前要先做一些准备工作,先写一个普通青年版,无脑notifyDataSetChanged()刷新的Demo。

1 一个普通的JavaBean,但是实现了clone方法,仅用于写Demo模拟刷新用,实际项目不需要,因为刷新时,数据都是从网络拉取的。:

class TestBean implements Cloneable {
 private String name;
 private String desc;
 ....//get set方法省略
 //仅写DEMO 用 实现克隆方法
 @Override
 public TestBean clone() throws CloneNotSupportedException {
  TestBean bean = null;
  try {
   bean = (TestBean) super.clone();
  } catch (CloneNotSupportedException e) {
   e.printStackTrace();
  }
  return bean;
 }

2 实现一个普普通通的RecyclerView.Adapter。

public class DiffAdapter extends RecyclerView.Adapter<DiffAdapter.DiffVH> {
 private final static String TAG = "zxt";
 private List<TestBean> mDatas;
 private Context mContext;
 private LayoutInflater mInflater;
 public DiffAdapter(Context mContext, List<TestBean> mDatas) {
  this.mContext = mContext;
  this.mDatas = mDatas;
  mInflater = LayoutInflater.from(mContext);
 }
 public void setDatas(List<TestBean> mDatas) {
  this.mDatas = mDatas;
 }
 @Override
 public DiffVH onCreateViewHolder(ViewGroup parent, int viewType) {
  return new DiffVH(mInflater.inflate(R.layout.item_diff, parent, false));
 }
 @Override
 public void onBindViewHolder(final DiffVH holder, final int position) {
  TestBean bean = mDatas.get(position);
  holder.tv1.setText(bean.getName());
  holder.tv2.setText(bean.getDesc());
  holder.iv.setImageResource(bean.getPic());
 }
 @Override
 public int getItemCount() {
  return mDatas != null ? mDatas.size() : 0;
 }
 class DiffVH extends RecyclerView.ViewHolder {
  TextView tv1, tv2;
  ImageView iv;
  public DiffVH(View itemView) {
   super(itemView);
   tv1 = (TextView) itemView.findViewById(R.id.tv1);
   tv2 = (TextView) itemView.findViewById(R.id.tv2);
   iv = (ImageView) itemView.findViewById(R.id.iv);
  }
 }
}

3 Activity كود:;

public class MainActivity extends AppCompatActivity {
 private List<TestBean> mDatas;
 private RecyclerView mRv;
 private DiffAdapter mAdapter;
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  initData();
  mRv = (RecyclerView) findViewById(R.id.rv);
  mRv.setLayoutManager(new LinearLayoutManager(this));
  mAdapter = new DiffAdapter(this, mDatas);
  mRv.setAdapter(mAdapter);
 }
 private void initData() {
  mDatas = new ArrayList<>();
  mDatas.add(new TestBean("张旭童1", "Android", R.drawable.pic1));
  mDatas.add(new TestBean("张旭童2", "Java", R.drawable.pic2));
  mDatas.add(new TestBean("张旭童3", "背锅", R.drawable.pic3));
  mDatas.add(new TestBean("张旭童4", "手撕产品", R.drawable.pic4));
  mDatas.add(new TestBean("张旭童5", "手撕测试", R.drawable.pic5));
 }
 /**
  * تحديث محاكاة
  *
  * @param view
  */
 public void onRefresh(View view) {
  try {
   List<TestBean> newDatas = new ArrayList<>();
   for (TestBean bean : mDatas) {
    newDatas.add(bean.clone());//نسخ بيانات القديمة مرة أخرى، لتشغيل عملية تحديث
   }
   newDatas.add(new TestBean("赵子龙", "帅", R.drawable.pic6));//تم إضافة بيانات جديدة
   newDatas.get(0).setDesc("Android+");
   newDatas.get(0).setPic(R.drawable.pic7); // تمثيل تعديل البيانات
   TestBean testBean = newDatas.get(1); // تمثيل تحريك البيانات
   newDatas.remove(testBean);
   newDatas.add(testBean);
   // لا تنسى تزويد Adapter ببيانات جديدة
   mDatas = newDatas;
   mAdapter.setDatas(mDatas);
   mAdapter.notifyDataSetChanged(); // كان يمكننا القيام بذلك في معظم الحالات السابقة
  } catch (CloneNotSupportedException e) {
   e.printStackTrace();
  }
 }
}

بسيط جداً، ولكن عند بناء مصدر البيانات الجديد newDatas، يتم استدعاء مصدر البيانات القديم mDatas، وتطبيق كل من data على clone() لضمان أن المصدر القديم والجديد يحتويان على نفس البيانات، ولكن العناوين (المراجع) مختلفة، لذلك عند تعديل القيم في newDatas، لن يؤثر ذلك على القيم في mDatas.

4 activity_main.xml تم حذف بعض الكود لعرض الأبعاد، وهي RecyclerView وButton لتمثيل التحديث.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
>
 <android.support.v7.widget.RecyclerView
  android:id="@+id/rv" />
 <Button
  android:id="@+id/btnRefresh"
  android:layout_alignParentRight="true"
  android:onClick="onRefresh"
  android:text="تمثيل التحديث" />
</RelativeLayout>

الآن هو مثال بسيط لشاب عادي يمكنه كتابته بسهولة، بدون تفكير في استخدام notifyDataSetChanged()، والنتيجة كما في الشكل الأول في الفصل الأول.
لكننا جميعاً نحاول أن نكون شباباً ثقافياً، إذن

تبدأ الآن في الوصول إلى الموضوع الرئيسي، استخدامه البسيط لـ DiffUtil، نحتاج فقط إلى كتابة فئة إضافية.

لنكون شعراء، يجب علينا إنشاء كائن يرث من DiffUtil.Callback ويقوم بتنفيذ أربعة أساليب abstract.
بالرغم من أن هذا الكائن يُدعى Callback، إلا أن فهمه على أنه كائن يحدد بعض العقد (Contract) والقواعد (Rule) لتحديد ما إذا كانت العناصر القديمة والجديدة متطابقة هو أكثر صحة.

تعريف فئة DiffUtil.Callback التالية:

 public abstract static class Callback {
  public abstract int getOldListSize(); // حجم مجموعة البيانات القديمة
  public abstract int getNewListSize(); // حجم مجموعة البيانات الجديدة
  public abstract boolean areItemsTheSame(int oldItemPosition, int newItemPosition); // هل العنصر في نفس الموقع في مجموعة البيانات القديمة والجديدة هو نفس الكائن؟ (قد يكون المحتوى مختلفًا، إذا تم إرجاع true من هنا، سيتم استدعاء الأسلوب التالي)
  public abstract boolean areContentsTheSame(int oldItemPosition, int newItemPosition); // يتم استدعاء هذا الأسلوب فقط إذا كان إعادة التغيير العرضي للمنطقة قيد الاستدعاء، أفهم أن notifyItemRangeChanged() هو الذي يتم استدعاؤه، ويجب التحقق مما إذا كان محتوى العنصر قد تغير
  // يتم استخدام هذا الأسلوب في الاستخدام المتقدم لـ DiffUtil، لن نتحدث عنه الآن
  @Nullable
  public Object getChangePayload(int oldItemPosition, int newItemPosition) {
   return null;
  }
 }

  هذا الديمو يوضح كيفية تنفيذ DiffUtil.Callback، ويأتي بتعليقات مزدوجة باللغتين الصينية والإنجليزية للأساليب الرئيسية (ما يعنيه ذلك هو أننا نترجم التعليقات الرسمية باللغة الإنجليزية، مما يساعد الجميع على فهمها بشكل أفضل).

/**
 * شرح: فئة رئيسية تستخدم لتحديد ما إذا كانت العناصر القديمة والجديدة متطابقة
 * مؤلف: zhangxutong
 * بريد إلكتروني: [email protected]
 * وقت: 2016/9/12.
 */
public class DiffCallBack extends DiffUtil.Callback {
 private List<TestBean> mOldDatas, mNewDatas; // تنبيه: يرجى النظر في الأسماء
 public DiffCallBack(List<TestBean> mOldDatas, List<TestBean> mNewDatas) {
  this.mOldDatas = mOldDatas;
  this.mNewDatas = mNewDatas;
 }
 // حجم مجموعة البيانات القديمة
 @Override
 public int getOldListSize() {
  return mOldDatas != null ? mOldDatas.size() : 0;
 }
 // حجم مجموعة البيانات الجديدة
 @Override
 public int getNewListSize() {
  return mNewDatas != null ? mNewDatas.size() : 0;
 }
 /**
  * يتم استدعاء DiffUtil لتحديد ما إذا كان الـ objectان يمثلان نفس الـ Item.
  * يتم استدعاء DiffUtil لتقرير ما إذا كان الـ objectان يمثلان نفس الـ Item.
  * على سبيل المثال، إذا كان لديك items يحتويون على ids فريدة، يجب أن يتحقق هذا الأسلوب من تساوي الـ id.
  * على سبيل المثال، إذا كان لديك Item يحتوي على حقل id فريد، فإن هذا الأسلوب يتحقق من تساوي الـ id.
  * في هذا المثال، يتم�断 ما إذا كانت حقل الـ name متطابقة.
  *
  * @param oldItemPosition الموقع الخاص بالعنصر في القائمة القديمة
  * @param newItemPosition Position of the item in the new list
  * @return True إذا كان الـ itemان يمثلان نفس الكائن أو false إذا كانا مختلفين.
  */
 @Override
 public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
  return mOldDatas.get(oldItemPosition).getName().equals(mNewDatas.get(newItemPosition).getName());
 }
 /**
  * يتم استدعاء DiffUtil عندما يريد التحقق مما إذا كان الـ itemان يحتويان على نفس البيانات.
  * يتم استدعاء DiffUtil لتحقق ما إذا كان يحتوي الـ itemان على نفس البيانات.
  * يستخدم DiffUtil هذه المعلومات للتحقق مما إذا كان محتوى عنصر قد تغير.
  * يستخدم DiffUtil المعلومات العودة (صحيح خطأ) للتحقق مما إذا كان محتوى العنصر قد تغير
  * يستخدم DiffUtil هذه الطريقة لتحقق من التطابق بدلاً من @link Object#equals(Object)}
  * يستخدم DiffUtil هذه الطريقة لتحقق من التطابق بدلاً من equals method.
  لذا يمكنك تغيير سلوكها بناءً على واجهة المستخدم الخاصة بك.
  * لذا يمكنك تغيير قيمة العودة بناءً على واجهة المستخدم الخاصة بك
  * على سبيل المثال، إذا كنت تستخدم DiffUtil مع
  * {@link android.support.v7.widget.RecyclerView.Adapter RecyclerView.Adapter}, يجب عليك
  * العودة بناءً على ما إذا كانت تمثيلات العناصر البصرية متطابقة.
  * على سبيل المثال، إذا كنت تستخدم RecyclerView.Adapter مع DiffUtil، يجب عليك العودة ببديل الظاهرية للعنصر.
  * يتم استدعاء هذه الطريقة فقط إذا كان @link #areItemsTheSame(int, int)} يعود بالصحة.
  * {@code true} لهذه العناصر.
  * هذا الطريقة يتم استدعاؤها فقط إذا كان @link #areItemsTheSame(int, int)} يعود بالصحة.
  * @param oldItemPosition الموقع الخاص بالعنصر في القائمة القديمة
  * @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.
  */
 @Override
 public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
  TestBean beanOld = mOldDatas.get(oldItemPosition);
  TestBean beanNew = mNewDatas.get(newItemPosition);
  if (!beanOld.getDesc().equals(beanNew.getDesc())) {
   return false; //إذا كانت المحتويات مختلفة، فأرجع false
  }
  if (beanOld.getPic() != beanNew.getPic()) {
   return false; //إذا كانت المحتويات مختلفة، فأرجع false
  }
  return true; //بالتقدير أن محتويات الdata متطابقة
 }

أعطيت هذه التعليقات التفصيلية والكود البسيط، يجب أن يكون من الواضح على الفور.

ثم عند الاستخدام، قم بإزالة comment من method notifyDatasetChanged() الذي كتبته من قبل، واستبدله بالكود التالي:

//محبوب جديد للشباب الفنيين
//استخدام method calculateDiff() من DiffUtil، وتقديم object DiffUtil.Callback كقاعدة، وboolean variable لتحديد ما إذا كان يجب اكتشاف تحرك item، للحصول على object DiffUtil.DiffResult
DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, newDatas), true);
//استخدام method dispatchUpdatesTo() من object DiffUtil.DiffResult، وتقديم Adapter من RecyclerView، يصبح بسهولة شابًا فنيًا
diffResult.dispatchUpdatesTo(mAdapter);
// لا تنسى تزويد Adapter ببيانات جديدة
mDatas = newDatas;
mAdapter.setDatas(mDatas);

شرح:

الخطوة الأولى

قبل تعيين newDatas إلى Adapter، قم أولاً بتهيئة method DiffUtil.calculateDiff()حساب أقل مجموعة تحديثات ممكنة للتحويل بين مجموعات البيانات القديمة والجديدة، وهو

object DiffUtil.DiffResult
تم تعريف method DiffUtil.calculateDiff() كما يلي:
الparameter الأول هو object DiffUtil.Callback
الparameter الثاني يمثل ما إذا كان يجب اكتشاف تحرك Item، يمكن تعديلها إلى false لزيادة كفاءة الخوارزمية، وتعيينها حسب الحاجة، ونحن هنا نستخدم true.

public static DiffResult calculateDiff(Callback cb, boolean detectMoves)

الخطوة الثانية

ثم يستخدم طريقة dispatchUpdatesTo() من كائن DiffUtil.DiffResult، ويتم إدخال Adapter لـ RecyclerView، لتحل محل طريقة notifyDatasetChanged() التي تستخدمها الشباب العاديين.

باستطلاع الكود المصدر، يمكن ملاحظة أن هذه الطريقة تدعو إلى استخدام أربعة طرق تحديث مخصصة لـ adapter.

 public void dispatchUpdatesTo(final RecyclerView.Adapter adapter) {
   dispatchUpdatesTo(new ListUpdateCallback() {
    @Override
    public void onInserted(int position, int count) {
     adapter.notifyItemRangeInserted(position, count);
    }
    @Override
    public void onRemoved(int position, int count) {
     adapter.notifyItemRangeRemoved(position, count);
    }
    @Override
    public void onMoved(int fromPosition, int toPosition) {
     adapter.notifyItemMoved(fromPosition, toPosition);
    }
    @Override
    public void onChanged(int position, int count, Object payload) {
     adapter.notifyItemRangeChanged(position, count, payload);
    }
   });
  }

ملخص:

لذلك، DiffUtil ليس فقط يمكنه العمل مع RecyclerView، يمكننا أيضًا تنفيذ أربعة طرق من واجهة ListUpdateCallback للقيام ببعض الأمور. (أنا أفكر في هذا الوقت بشكل عشوائي، هل يمكنه العمل مع وحدة التحكم في التخطيط الخاص بي؟ أو تحسين NestFullListView الذي كتبته في المقالة السابقة؟ نعم، هذا هو الحل الجيد للListView وRecyclerView وScrollView المدمجة في ListView: http://blog.csdn.net/zxt0601/article/details/52494665)

إلى هنا، نحن قد تطورنا إلى عشاق الأدب، ونتيجة التشغيل تكون مشابهة للصورة الثانية في الفصل الأول،

الفرق الوحيد هو أن نهج adapter.notifyItemRangeChanged() سيكون هناك تأثير لشعاع البيت الكامل في تحديث الحركة (موضع Demo هو item مع position 0). هذه الحركة الشعاعية في البيت تحبها بعض الناس وكرهها بعضهم، ولكن هذا لا يهم،

لأننا عندما نتعلم استعمال DiffUtil المتقدم في الفصل الثالث، لا يهم إن كنت تحب هذا ItemChange animation أو لا، سيذهب مع الرياح. (لا أعرف إذا كان هناك خطأ في البرمجة الرسمية)
النتيجة تكون مثل الصورة الثانية في الفصل الأول، item0 الخاص بنا في الواقع تغيرت الصورة والنص، ولكن هذه التغييرات لم تأتي مع أي حركة.

دعونا نتحرك نحو طريق عشاق الأدب في عالم عشاق الأدب.

ثلاثة استعمالات متقدمة لـ DiffUtil

النظرية:

الاستخدام المتقدم فقط يتضمن نهجين،
نحتاج إلى تنفيذ نهج DiffUtil.Callback منفردًا،
منظمة Object getChangePayload(int oldItemPosition, int newItemPosition) نهج،
العنصر الذي يعود به يعني ما تغير من محتويات العنصر.

مؤتمتة مع RecyclerView.Adapter نهج،
منظمة void onBindViewHolder(VH holder, int position, List<Object> payloads) نهج،
إكمال تحديث التوجيه. (تكون أنت من عشاق الأدب في عالم عشاق الأدب، عشاق الأدب الأدباء.)
الخط الأحمر، هذا نهج جديد، انتبه أن لديه ثلاثة معلمات، المعلمتان الأولتان نعرفهما، والمعلمة الثالثة تحتوي على الأشياء التي نعود بها من getChangePayload().

حسنًا، لنبدأ نحن بمشاهدة هوية هذا النهج:

في رمز المصدر v7-24.2.0، يبدو هذا النهج كالتالي:

 /**
   * يتم استدعاء هذا النهج بواسطة RecyclerView لعرض البيانات في الموضع المحدد. هذا النهج
   * يجب تحديث محتويات {@link ViewHolder#itemView} ليعكس العنصر في
   * الموضع المحدد.
   * <p>
   * تنبيه أن على عكس {@link android.widget.ListView}، لن يُطلق RecyclerView هذا النهج
   * مرة أخرى إذا تغير موقف العنصر في مجموعة البيانات إلا إذا كان العنصر نفسه}}
   * منتهية الصلاحية أو لا يمكن تحديد الموقع الجديد. لهذا السبب، يجب أن تستخدم فقط
   * استخدم معامل <code>position</code> عند الحصول على العنصر البياني المرتبط داخل
   * هذا الطريقة وعدم الحفاظ على نسخة منها. إذا كنت بحاجة إلى موقف العنصر لاحقًا،
   * في (على سبيل المثال، في مستمع النقر)، استخدم {@link ViewHolder#getAdapterPosition()} الذي سي
   * يحتوي على موقف الموجه المعدل.
   * <p>
   * مقارنة بين bind جزئي وـ bind كامل:
   * <p>
   * هو قائمة دمج من {@link #notifyItemChanged(int, Object)} أو
   * {@link #notifyItemRangeChanged(int, int, Object)}. إذا لم تكن قائمة البيانات المهمة فارغة،
   * الحامل حاليًا موصول إلى بيانات قديمة وأن الموجه قد يقوم بـ bind جزئي فعال
   * تحديث باستخدام معلومات البيانات. إذا كان البيانات فارغة، يجب على الموجه تشغيل bind كامل.
   * الموجه يجب ألا يتوقع أن البيانات التي يتم تمريرها إلى طرق الإشعار ستتم استلامها من
   * onBindViewHolder(). على سبيل المثال، عندما يتم إدراج الرؤية في الشاشة،}}
   * المواد في notifyItemChange() سيتم التخلص منها ببساطة.
   *
   * @param holder ViewHolder يجب تحديثه لتقديم محتويات
   *    العنصر في الموقع المحدد في مجموعة البيانات.
   * @param position موقع العنصر داخل مجموعة بيانات الأداة.
   * @param payloads قائمة غير فارغة من المواد المدمجة. يمكن أن تكون قائمة فارغة إذا كان يتطلب الكامل
   *     تحديث.
   */
  public void onBindViewHolder(VH holder, int position, List<Object> payloads) {
   onBindViewHolder(holder, position);
  }

أصلاً، إنها تدعو إلى onBindViewHolder بثلاثة معلمات فقط (ملاحظة جانبية، أوه، حبيبي، Adapter الخاص بي NestFullListView يشبه أيضًا هذا الشكل، يبدو أنني أقترب أكثر من معلم Google الكبير)

عندما رأيت هذا، أدركت أن مدخل onBinds هو هذا الطريقة، وهي الطريقة التي تتطابق مع onCreateViewHolder،
إذا نظرت إلى الأسفل في الكود لبضع أسطر، يمكنك رؤية public final void bindViewHolder(VH holder, int position)، والتي تدعو إلى onBindViewHolder بثلاثة معلمات.
حول RecyclerView.Adapter أيضًا، لا يمكن توضيحها في بضع كلمات فقط. (في الواقع، لم أتعلم سوى هذا)
حسنًا، لسنا ننتج عن الموضوع، نعود إلى طريقة ثلاثية الوصف onBindViewHolder(VH holder, int position, List<Object> payloads)، في بداية هذه الطريقة هناك الكثير من التعليقات باللغة الإنجليزية، لطالما أعتقدت أن قراءة هذه التعليقات مفيدة جدًا لفهم الطريقة، لذا قمت بترجمتها،

الترجمة:

تُستدعى من قبل RecyclerView لعرض البيانات في الموضع المحدد.
يجب أن تتمكن هذه الطريقة من تحديث محتوى ItemView في ViewHolder ليعكس التغيير في العنصر المحدد.
لاحظ أن، على عكس ListView، لا تقوم RecyclerView بتوليد هذا الطريقة مرة أخرى إذا تغيرت بيانات مجموعة العناصر في الموضع المحدد، إلا إذا كان العنصر نفسه معطلًا (invalidated) أو إذا لم يمكن تحديد الموضع الجديد.
لذلك، في هذه الطريقة، يجب عليك استخدام معامل position فقط لاستخراج بيانات العنصر ذات الصلة، وليس للحفاظ على نسخة من بيانات العنصر هذه.
إذا كنت بحاجة لاحقًا إلى موقع هذا العنصر، مثل إعداد clickListener، يجب استخدام ViewHolder.getAdapterPosition()، التي تقدم الموقع المعدل.
عندما رأيت هذا الكاتب هنا، وجدت أن هذا هو شرح طريقة onbindViewHolder الثنائية الأجزاء.
هذا هو الجزء الفريد من هذه الطريقة الثلاثية الأجزاء:)
**الترابط الجزئي (partial) مقابل الترابط الكامل (full)**
موضوع النقل payloads هو قائمة مدمجة من (notifyItemChanged(int, Object) أو notifyItemRangeChanged(int, int, Object)).
إذا كان قائمة موضوعات النقل payloads غير فارغة، يمكن استخدام ViewHolder الحالي الم绑定 ببيانات القديمة وAdapter للقيام بتحديث جزئي فعال.
إذا كان موضوع النقل payloads فارغًا، يجب على Adapter إجراء ترابط كامل مرة واحدة (تقديم طريقة بثلاثة أجزاء).
لا يجب أن يفترض Adapter (يأخذها كأمر مسلوق) أن موضوع النقل الذي يتم تمريره في طرق الاشعار notifyxxxx سيكون متاحًا في طريقة onBindViewHolder() (هذا السطر صعب الترجمة QAQ يرجى الرجوع إلى المثال).
على سبيل المثال، عندما يكون View غير متصل في الشاشة، يمكن رمي موضوع النقل هذا من notifyItemChange() ببساطة.
لا يمكن أن يكون موضوع النقل payloads null، ولكن يمكن أن يكون فارغًا (empty)، في هذه الحالة يجب الترابط الكامل (لذا نحن فقط نقوم بالتحقق من isEmpty في الطريقة، دون الحاجة إلى التحقق من الفارغة مرة أخرى).
الكاتب يقول: هذا الأسلوب هو أسلوب فعال. أنا مترجم غير فعال، استغرقت 40+ دقيقة. حتى أدركت أن الجزء المهم تم عرضه مسبقًا بالتقديم.

التطبيق العملي:

بعد كل هذا الكلام، في الواقع الاستخدام بسيط جدًا:
دعنا نبدأ باستخدام طريقة getChangePayload()، وستكون هناك أيضًا ملاحظات باللغتين الصينية والإنجليزية

  

 /**
  * عندما يعود @link #areItemsTheSame(int, int)} بـtrue لكل من العنصرين،
  * يعود @link #areContentsTheSame(int, int)} بـfalse لهم، DiffUtil
  * ينفذ هذا الطريقة للحصول على حامل التغيير المتعلق بالتغيير.
  * 
  * عندما يعود @link #areItemsTheSame(int, int)} بـtrue، ويكون @link #areContentsTheSame(int, int)} يعود بـfalse، DiffUtils سينفذ هذا الطريقة،
  * للحصول على حامل التغيير الخاص بهذا العنصر (ما الذي تغير).
  * 
  * على سبيل المثال، إذا كنت تستخدم DiffUtil مع @link RecyclerView)، يمكنك العودة بالـ
  * الحقل المحدد الذي تغير في العنصر وجميع
  * يمكن استخدام @link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} ذلك
  * المعلومات لتنفيذ الحركة الصحيحة.
  * 
  * على سبيل المثال، إذا كنت تستخدم RecyclerView مع DiffUtils، يمكنك العودة بهذه الحقول التي تغيرت في العنصر،
  * يمكن استخدام @link android.support.v7.widget.RecyclerView.ItemAnimator ItemAnimator} لتلك المعلومات لتنفيذ الحركة الصحيحة
  * 
  * تنفيذ المعيارية يعود بـnull. * تنفيذ المعيارية هو عودة null
  *
  * @param oldItemPosition الموقع الخاص بالعنصر في القائمة القديمة
  * @param newItemPosition Position of the item in the new list
  * @return Payload object represents the change between the two items.
  * يعود payload يمثل تغيير محتويات item القديم والجديد
  */
 @Nullable
 @Override
 public Object getChangePayload(int oldItemPosition, int newItemPosition) {
  // بتنفيذ هذه الطريقة يمكنك أن تصبح شاعر الشعراء في الشعراء
  // تحديث جزئي في التحديث الموجه
  // الأكثر كفاءة
  // لا يوجد الآن تأثير ItemChange من الضوء الأبيض (على الرغم من أنني أعتقد أن هذا ليس مهمًا جدًا)
  TestBean oldBean = mOldDatas.get(oldItemPosition);
  TestBean newBean = mNewDatas.get(newItemPosition);
  // هنا لا تحتاج إلى مقارنة الحقول الأساسية، لأنها متطابقة دائمًا
  Bundle payload = new Bundle();
  if (!oldBean.getDesc().equals(newBean.getDesc())) {
   payload.putString("KEY_DESC", newBean.getDesc());
  }
  if (oldBean.getPic() != newBean.getPic()) {
   payload.putInt("KEY_PIC", newBean.getPic());
  }
  if (payload.size() == 0) // إذا لم يكن هناك تغيير يتم إرسال فارغ
   return null;
  return payload; //
 }

بشكل بسيط، هذه الطريقة تعود Object نوع payload يحتوي على محتويات item التي تغيرت.
نحن هنا نستخدم Bundle لحفظ هذه التغييرات.

في Adapter يتم إعادة كتابة onBindViewHolder الثلاثي الم参数 كما يلي:

 @Override
 public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) {
  if (payloads.isEmpty()) {}}
   onBindViewHolder(holder, position);
  } else {
   // الشباب الفني في الشباب الفنيين
   Bundle payload = (Bundle) payloads.get(0);
   TestBean bean = mDatas.get(position);
   for (String key : payload.keySet()) {
    switch (key) {
     case "KEY_DESC":
      // يمكن استخدام بيانات payload، ولكن البيانات الجديدة كذلك يمكن استخدامها
      holder.tv2.setText(bean.getDesc());
      break;
     case "KEY_PIC":
      holder.iv.setImageResource(payload.getInt(key));
      break;
     default:
      break;
    }
   }
  }
 }

هناك payloads هي قائمة، ويمكن رؤيتها من التعليقات أنها ليست null، لذا نقوم بالتحقق مما إذا كان empty
إذا كان empty، يتم استدعاء دالة بثلاثة معلمات، لإجراء bind كامل
إذا لم يكن empty، يتم إجراء bind جزئي
يتم استخراج payload الذي نرجعه في getChangePayload باستخدام العنصر 0، ثم تمريره عبر keys في payload، واستخراج التغييرات الموجودة في payload إذا كانت موجودة، ثم تحديثها في ItemView.
(في هذا السياق، يتم الحصول على بيانات المصدر الأحدث من خلال mDatas، لذا يمكن تحديث البيانات باستخدام بيانات payload أو بيانات البيانات الجديدة)

حتى الآن، لقد ملكنا كيفية تحديث RecyclerView، وكيفية كتابة الشيفرة بشكل فني في فئة الشباب الفنيين

الجزء الرابع: استخدام DiffUtil في الخلفية

تقدم تعليقات الرأس في ملف المصدر لـ DiffUtil معلومات حول DiffUtil
يستخدم DiffUtil خوارزمية Eugene W. Myers's difference، ولكن هذه الخوارزمية لا يمكنها اكتشاف الحركة للعناصر، لذا قام Google بتحسينها لدعم اكتشاف الحركة للعناصر، ولكن اكتشاف الحركة للعناصر يتطلب موارد إضافية.
عند وجود 1000 عنصر بيانات، 200 تغيير، استغرقت هذه الألگوريثم:
عند فتح تشخيص الحركة: المتوسط: 27.07ms، المعدل المتوسط: 26.92ms.
عند إغلاق تشخيص الحركة: المتوسط: 13.54ms، المعدل المتوسط: 13.36ms.
有兴趣可以自行去源码头部阅读注释,对我们比较有用的是其中一段提到,
إذا كنت مهتماً، يمكنك قراءة التعليقات في بداية المصدر، التي ستكون مفيدة لنا هي الجزء الذي ذكر فيه،

إذا كانت قائمة لدينا كبيرة، فإن وقت حساب DiffResult سيكون طويلاً، لذا يجب علينا وضع عملية الحصول على DiffResult في نواة فرعية، وتحديث RecyclerView في نواة الرئيسية.

كود كما يلي:

 private static final int H_CODE_UPDATE = 1;
 private List<TestBean> mNewDatas; // إضافة متغير لتخزين newList مؤقتاً
 private Handler mHandler = new Handler() {
  @Override
  public void handleMessage(Message msg) {
   switch (msg.what) {
    case H_CODE_UPDATE:
     // يتم استخراج النتيجة
     DiffUtil.DiffResult diffResult = (DiffUtil.DiffResult) msg.obj;
     diffResult.dispatchUpdatesTo(mAdapter);
     // لا تنسى تزويد Adapter ببيانات جديدة
     mDatas = mNewDatas;
     mAdapter.setDatas(mDatas);
     break;
   }
  }
 });
   new Thread(new Runnable() {
    @Override
    public void run() {
     // يتم حساب DiffResult في نواة فرعية
     DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffCallBack(mDatas, mNewDatas), true);
     Message message = mHandler.obtainMessage(H_CODE_UPDATE);
     message.obj = diffResult; // obj يحتوي على DiffResult
     message.sendToTarget();
    }
   }).start();

بسيط استعمال مدير التعامل، لا أريد التكرار.

خلاصة والآخرين

1 في الواقع كمية الكود في هذا المقال قليلة، يمكنك تنزيل Demo لمعاينة، هناك فقط أربعة كلاسات.
لكن بلا وعي، أصبحت هذه المقالة طويلة جدًا، حيث تتضمن بعض التعليقات على الرموز المصدر، مما يساعد الجميع على فهم أفضل.

2 DiffUtil مثالي لهذا النوع من السيناريوهات مثل التمرير لأسفل للتحديث،
أزالة الكود، ويمكنك استخدام الرسوم البيانية، وأيضًا ~ لا تحتاج إلى التفكير في العقل.
لكن إذا كنت بحاجة إلى القيام ببعض الأشياء مثل حذف الأعمدة أو إعجاب، فلا تحتاج إلى استخدام DiffUtils. فقط قم بتذكر position، قم بتحديد ما إذا كان position موجودًا في الشاشة، واستدعاء هذه الطرق التوجيهية لتحديث الشاشة.

3 في الواقع، DiffUtil ليس مجرد شيء يمكن استخدامه مع RecyclerView.Adapter،
يمكننا تنفيذ واجهة ListUpdateCallback بأنفسنا، ونستخدم DiffUtil لمساعدتنا في العثور على أكبر الفروق بين مجموعة البيانات القديمة والجديدة لفعل المزيد.

4 ملاحظة: عند كتابة DEMO، مجموعة البيانات القديمة والجديدة التي يتم مقارنتها، يجب أن تكون ليست فقط ArrayList مختلفة، بل يجب أن تكون كل data داخلها مختلفة أيضًا. وإلا لن يتم تفعيل changed.
في المشاريع الفعلية، لا يمكن العثور عليها، لأن البيانات الجديدة تأتي غالبًا من الإنترنت.

اليوم هو اليوم الأخير من عيد الأضحى، حيث بدأ شركتي في العمل من جديد!!!في الغضب، كتبت مقالًا عن DiffUtil، وأنا لا أحتاج إلى DiffUtil أيضًا، يمكنني مقارنة الفروق بين شركتي وشركات أخرى بسهولة. QAQ، وكنت في حالة سيئة اليوم، استغرقت ثماني ساعات للانتهاء من المقال. ظننت أن هذا المقال يمكن أن يدخل في مجموعة المقالات القصيرة، لكنه كان طويلاً جدًا. يمكن للذين ليس لديهم صبر أن يتحققوا من DEMO، لا يوجد الكثير من الكود، ويكون الاستخدام سهلاً.

الصفحة الرئيسية github:
https://github.com/mcxtzhang/DiffUtils

بالإضافة إلى ذلك، هذا هو تنظيم المعلومات حول مكتبة DiffUtil للوحة Android7.0، سيتم إكمال المزيد من المعلومات في المستقبل، شكرًا لكم على دعم هذا الموقع!

توصيات