0Pricing
Android Academy · درس

Adapters وDiffUtil

أنشئوا adapters فعّالة ومتحركة للقوائم باستخدام ListAdapter وDiffUtil.ItemCallback. استخدموا submitList() لإجراء تحديثات سلسة وجزئية.

Adapters وDiffUtil درس مجاني في Android Academy على CoddyKit. هذا هو الدرس 6 من أصل 6. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في Android Academy، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة Android Academy 6 دروس في المجموع.

مشكلة notifyDataSetChanged()

عند تغيّر بيانات RecyclerView، تؤدي استدعاء notifyDataSetChanged() إلى إعادة رسم كل العناصر، حتى العناصر التي لم تتغيّر. وينتج عن ذلك:

  • عدم وجود تحريكات للعناصر
  • أداء ضعيف في القوائم الكبيرة
  • وميض واجهة المستخدم

يحسب DiffUtil أدنى مجموعة من التغييرات ويطبّقها بتحريكات سلسة.

DiffUtil.ItemCallback

حدّد كيفية مقارنة عنصرين. هناك سؤالان يجب الإجابة عنهما:

  • areItemsTheSame — هل يمثلان العنصر المنطقي نفسه؟ (تُقارن المعرّفات عادةً)
  • areContentsTheSame — هل جميع الحقول الظاهرة متطابقة؟
data class Post(val id: Int, val title: String, val likes: Int)

class PostDiffCallback : DiffUtil.ItemCallback<Post>() {

    override fun areItemsTheSame(oldItem: Post, newItem: Post): Boolean {
        return oldItem.id == newItem.id   // same post?
    }

    override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean {
        return oldItem == newItem         // data class equality (all fields)
    }
}

ListAdapter

ListAdapter هو RecyclerView.Adapter مضمّن فيه DiffUtil. مرّر ItemCallback إلى مُنشئه واستدعِ submitList() لتحديث البيانات:

class PostAdapter : ListAdapter<Post, PostAdapter.ViewHolder>(PostDiffCallback()) {

    inner class ViewHolder(val binding: ItemPostBinding) : RecyclerView.ViewHolder(binding.root)

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        val binding = ItemPostBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return ViewHolder(binding)
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val post = getItem(position)   // use getItem(), not a manual list
        holder.binding.tvTitle.text = post.title
        holder.binding.tvLikes.text = "${post.likes} likes"
    }
}

إرسال قوائم جديدة

استدعِ submitList() كلما تغيّرت بياناتك. يقارن ListAdapter القائمة القديمة بالجديدة في خيط خلفي، ويطبّق التغييرات الضرورية فقط:

// In Activity or Fragment:
val adapter = PostAdapter()
binding.recyclerView.adapter = adapter

// Observe ViewModel LiveData:
viewModel.posts.observe(viewLifecycleOwner) { posts ->
    adapter.submitList(posts)   // diff calculated off main thread
}

// Important: always submit a NEW list, not a mutated old one
// WRONG:  currentList.add(newPost); adapter.submitList(currentList)
// RIGHT:  adapter.submitList(currentList + newPost)

معالجات النقر في ListAdapter

مرّر معالجات النقر عبر المُنشئ، فهذا نمط شائع ونظيف:

class PostAdapter(
    private val onLike: (Post) -> Unit,
    private val onOpen: (Post) -> Unit
) : ListAdapter<Post, PostAdapter.ViewHolder>(PostDiffCallback()) {

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
        val post = getItem(position)
        holder.binding.tvTitle.text = post.title
        holder.binding.btnLike.setOnClickListener { onLike(post) }
        holder.itemView.setOnClickListener { onOpen(post) }
    }
}

// Usage:
val adapter = PostAdapter(
    onLike = { viewModel.likePost(it) },
    onOpen = { navigateToDetail(it.id) }
)

getChangePayload() — التحديثات الجزئية

عندما يتغيّر حقل واحد فقط، مثل عدد الإعجابات، تجنّب إعادة الربط الكاملة باستخدام getChangePayload():

class PostDiffCallback : DiffUtil.ItemCallback<Post>() {
    override fun areItemsTheSame(oldItem: Post, newItem: Post) = oldItem.id == newItem.id
    override fun areContentsTheSame(oldItem: Post, newItem: Post) = oldItem == newItem

    override fun getChangePayload(oldItem: Post, newItem: Post): Any? {
        // Return non-null payload to trigger partial bind
        return if (oldItem.likes != newItem.likes) "likes_changed" else null
    }
}

// In Adapter.onBindViewHolder with payloads:
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: List<Any>) {
    if (payloads.contains("likes_changed")) {
        holder.binding.tvLikes.text = "${getItem(position).likes} likes"
    } else {
        super.onBindViewHolder(holder, position, payloads)
    }
}

أنواع عرض متعددة

استخدم getItemViewType() لعرض تخطيطات مختلفة للعناصر في القائمة نفسها:

class FeedAdapter : ListAdapter<FeedItem, RecyclerView.ViewHolder>(FeedDiffCallback()) {

    companion object {
        const val TYPE_POST = 0
        const val TYPE_AD   = 1
    }

    override fun getItemViewType(position: Int): Int {
        return when (getItem(position)) {
            is FeedItem.Post -> TYPE_POST
            is FeedItem.Ad   -> TYPE_AD
        }
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
        TYPE_POST -> PostViewHolder(ItemPostBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        else      -> AdViewHolder(ItemAdBinding.inflate(LayoutInflater.from(parent.context), parent, false))
    }
}

ConcatAdapter — تركيب القوائم

يجمع ConcatAdapter عدة adapters في RecyclerView واحد، وهو مناسب جدًا للترويسات والتذييلات وحالات التحميل:

// Separate adapters for different sections
val headerAdapter = HeaderAdapter()
val postAdapter   = PostAdapter()
val loaderAdapter = LoadingAdapter()

// Combine them in order
binding.recyclerView.adapter = ConcatAdapter(headerAdapter, postAdapter, loaderAdapter)

// Update each independently
postAdapter.submitList(posts)
loaderAdapter.showLoading(true)

AsyncListDiffer — تحكم يدوي

إذا تعذّر عليك تمديد ListAdapter، فاستخدم AsyncListDiffer مباشرةً:

class CustomAdapter : RecyclerView.Adapter<CustomAdapter.ViewHolder>() {

    private val differ = AsyncListDiffer(this, PostDiffCallback())

    fun submitList(list: List<Post>) = differ.submitList(list)
    fun getItem(position: Int): Post = differ.currentList[position]

    override fun getItemCount() = differ.currentList.size
}

نصائح لتحسين الأداء

حافظ على سرعة RecyclerView:

  • استخدم setHasStableIds(true) إذا كانت لعناصرك معرّفات فريدة وثابتة
  • تجنّب إنشاء كائنات داخل onBindViewHolder
  • استخدم RecycledViewPool لمشاركة العروض المعاد تدويرها بين عدة RecyclerViews
  • استخدم setItemViewCacheSize() لزيادة ذاكرة التخزين المؤقت خارج الشاشة

التحريكات متاحة تلقائيًا

يؤدي تكامل ListAdapter مع DiffUtil تلقائيًا إلى تشغيل DefaultItemAnimator من أجل:

  • تلاشي ظهور العناصر الجديدة
  • تلاشي اختفاء العناصر المحذوفة
  • تحريك العنصر عند تغيّر موضعه

عيّن محرّك تحريك مخصصًا: recyclerView.itemAnimator = MyAnimator(). عيّنه إلى null لتعطيل جميع التحريكات.

تحقق سريع

ما الطريقتان اللتان يجب تنفيذهما في DiffUtil.ItemCallback؟

مراجعة: Adapters وDiffUtil

قوائم فعّالة ومتحركة بأقل قدر من التعليمات البرمجية:

  • DiffUtil.ItemCallback — تعريف الهوية (areItemsTheSame) والتساوي (areContentsTheSame)
  • ListAdapter — مقارنة غير متزامنة مضمّنة؛ استدعِ submitList()
  • أرسل دائمًا مرجع قائمة جديدًا، ولا تعدّل القائمة الحالية مطلقًا
  • استخدم getChangePayload() لعمليات إعادة الربط الجزئية
  • استخدم ConcatAdapter لتركيب عدة adapters

التالي: التنقل بين شاشات متعددة باستخدام Navigation Component.

الأسئلة الشائعة

هل درس «Adapters وDiffUtil» مجاني؟

نعم — نص درس «Adapters وDiffUtil» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة Android Academy، انتقل إلى CoddyKit PRO. تتضمن دورة Android Academy 6 دروس في المجموع.

ماذا ستتعلم في «Adapters وDiffUtil»؟

أنشئوا adapters فعّالة ومتحركة للقوائم باستخدام ListAdapter وDiffUtil.ItemCallback. استخدموا submitList() لإجراء تحديثات سلسة وجزئية. تتمرن على Android Academy مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ Android Academy؟

لا تُشترط خبرة سابقة. Android Academy على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 6 من أصل 6.

كم من الوقت يستغرق درس «Adapters وDiffUtil»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس Android Academy هذا؟

نعم. كل درس في Android Academy يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. مجموعات Kotlin
  2. أساسيات RecyclerView
  3. أحداث النقر في RecyclerView
  4. SharedPreferences
  5. تفضيلات DataStore
  6. Adapters وDiffUtil
← العودة إلى Android Academy