0Pricing
Android Academy · บทเรียน

อะแดปเตอร์และ DiffUtil

สร้างอะแดปเตอร์รายการที่มีประสิทธิภาพและภาพเคลื่อนไหวด้วย ListAdapter และ DiffUtil.ItemCallback ใช้ submitList() เพื่ออัปเดตเฉพาะส่วนอย่างราบรื่น

อะแดปเตอร์และ DiffUtil เป็นบทเรียน Android Academy ฟรีบน CoddyKit นี่คือบทเรียนที่ 6 จากทั้งหมด 6 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Android Academy และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Android Academy มีบทเรียนทั้งหมด 6 บทเรียน

ปัญหาของ notifyDataSetChanged()

เมื่อข้อมูลใน RecyclerView เปลี่ยนแปลง การเรียกใช้ notifyDataSetChanged() จะวาดรายการ ทุก รายการใหม่ แม้แต่รายการที่ไม่เปลี่ยนแปลงก็ตาม ซึ่งทำให้เกิดผลดังนี้:

  • ไม่มีภาพเคลื่อนไหวของรายการ
  • ประสิทธิภาพต่ำเมื่อใช้รายการขนาดใหญ่
  • UI กะพริบ

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 รวมตัวปรับหลายตัวเป็น 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 เพื่อแชร์มุมมองที่นำกลับมาใช้ใหม่ระหว่าง RecyclerView หลายรายการ
  • ใช้ setItemViewCacheSize() เพื่อเพิ่มแคชสำหรับมุมมองนอกหน้าจอ

มีภาพเคลื่อนไหวให้โดยอัตโนมัติ

การผสานรวม DiffUtil ของ ListAdapter จะเรียกใช้ DefaultItemAnimator โดยอัตโนมัติสำหรับ:

  • การค่อย ๆ ปรากฏของรายการใหม่
  • การค่อย ๆ หายไปของรายการที่ถูกนำออก
  • ภาพเคลื่อนไหวการย้ายเมื่อรายการเปลี่ยนตำแหน่ง

กำหนดตัวทำภาพเคลื่อนไหวแบบกำหนดเองได้ด้วย recyclerView.itemAnimator = MyAnimator() กำหนดเป็น null เพื่อปิดภาพเคลื่อนไหวทั้งหมด

ตรวจสอบความเข้าใจ

คุณต้องใช้เมธอดสองเมธอดใดใน DiffUtil.ItemCallback

ทบทวน: ตัวปรับรายการและ DiffUtil

สร้างรายการที่มีประสิทธิภาพและภาพเคลื่อนไหวได้ด้วยโค้ดเพียงเล็กน้อย:

  • DiffUtil.ItemCallback — กำหนดเอกลักษณ์ (areItemsTheSame) และความเท่ากัน (areContentsTheSame)
  • ListAdapter — เปรียบเทียบข้อมูลแบบไม่พร้อมกันในตัว ให้เรียก submitList()
  • ส่งรายการอ้างอิงใหม่เสมอ อย่าแก้ไขรายการปัจจุบันโดยตรง
  • ใช้ getChangePayload() สำหรับการผูกข้อมูลใหม่บางส่วน
  • ใช้ ConcatAdapter เพื่อประกอบตัวปรับหลายตัว

ถัดไป: การนำทางหลายหน้าจอด้วย Navigation Component

คำถามที่พบบ่อย

บทเรียน “อะแดปเตอร์และ DiffUtil” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “อะแดปเตอร์และ DiffUtil” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Android Academy ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Android Academy มีบทเรียนทั้งหมด 6 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “อะแดปเตอร์และ DiffUtil”

สร้างอะแดปเตอร์รายการที่มีประสิทธิภาพและภาพเคลื่อนไหวด้วย ListAdapter และ DiffUtil.ItemCallback ใช้ submitList() เพื่ออัปเดตเฉพาะส่วนอย่างราบรื่น คุณปฏิบัติ Android Academy ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Android Academy หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Android Academy บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 6 จากทั้งหมด 6 บทเรียน

บทเรียน “อะแดปเตอร์และ DiffUtil” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Android Academy นี้ได้ไหม

ได้ บทเรียน Android Academy ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. คอลเลกชัน Kotlin
  2. พื้นฐาน RecyclerView
  3. เหตุการณ์คลิกใน RecyclerView
  4. SharedPreferences
  5. ค่ากำหนดของ DataStore
  6. อะแดปเตอร์และ DiffUtil
← กลับไปที่ Android Academy