11 April 2018

สร้าง RecyclerView แบบสบายๆด้วย ListAdapter จาก Support library

Updated on


       วันนี้แอบไปคุ้ย Support Library เล่นๆแล้วก็ได้พบเจอกับ ListAdapter ที่เอาไว้ใช้กับ RecyclerView ที่มีการทำงานที่เรียบง่ายแล้วน่าสนใจจนต้องหยิบเอามาเล่าให้ฟังกันฮะ

Adapter ของ Recycler View แบบเดิมๆ

        โดยปกติแล้วเวลาจะสร้าง RecyclerView เพื่อใช้งานซักตัวหนึ่ง ก็จะต้องสร้าง Adapter และ ViewHolder เนอะ และ Adapter จะมีลักษณะหน้าตาประมาณนี้

// PostAdapter.kt

class PostAdapter : RecyclerView.Adapter<PostViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
        return ...
    }

    override fun getItemCount(): Int {
        return ...
    }

    override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
        ...
    }
}

        และทุกครั้งก็จะต้องมีข้อมูลเก็บไว้ในนี้เนอะ ดังนั้นก็ต้องมี Setter ในนี้ด้วยและก็ต้องเขียนโค้ดใน getItemCount() เพื่อโยนจำนวนข้อมูลที่มี เพื่อให้ RecyclerView รู้ว่าข้อมูลทั้งหมดมีกี่ตัว

// PostAdapter.kt

class PostAdapter : RecyclerView.Adapter<PostViewHolder>() {
    var postList: List<Post>? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
        return ...
    }

    override fun getItemCount(): Int {
        return postList?.size ?: 0
    }

    override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
        ...
    }
}

        และถ้าอยากจะใช้ DiffUtil เพื่อที่จะได้ไม่ต้องจัดการกับข้อมูลเวลาที่ข้อมูลมีการเปลี่ยนแปลง ก็จะต้องสร้างคลาสเพิ่มอีกตัวแบบนี้

// PostDiffCallback.kt

class PostDiffCallback(var oldItemList: List<Post>, var newItemList: List<Post>) : DiffUtil.Callback() {
    override fun getOldListSize(): Int {
        return oldItemList.size
    }

    override fun getNewListSize(): Int {
        return newItemList.size
    }

    override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldItemList[oldItemPosition]
        val newItem = newItemList[newItemPosition]
        return oldItem.text == newItem.text && oldItem.timestamp == newItem.timestamp
    }

    override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
        val oldItem = oldItemList[oldItemPosition]
        val newItem = newItemList[newItemPosition]
        return oldItem.id == newItem.id
    }
}

        และเวลาเรียกใช้งานก็จะต้องมานั่งโยนข้อมูลเข้าไปใน DiffUtil แล้วสั่งให้ Adapter อัพเดทค่าเองแบบนี้

// MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var adapter: PostAdapter
    private var oldPostList: List<Post>? = null

    ...

    fun updatePostList(newPostList: List<Post>) {
        val oldPostList = this.oldPostList
        oldPostList?.let {
            val diffResult = DiffUtil.calculateDiff(PostDiffCallback(oldPostList, newPostList))
            diffResult.dispatchUpdatesTo(adapter)
        }
        this.oldPostList = newPostList
    }
}

        คือจริงๆแล้ว DiffUtil นี่มันเจ๋งนะ แต่ว่าไม่ค่อยน่าประทับใจซักเท่าไรตอนที่ Implement มันนี่แหละ จึงอาจจะเป็นส่วนหนึ่งที่ทำให้ DiffUtil ไม่ค่อยถูกหยิบมาใช้งานกันซักเท่าไร

ListAdapter ก็คือ Built-in DiffUtil Adapter นั่นเอง

        เพื่อให้สะดวกสบายมากขึ้น ทีม Support Library จึงเพิ่ม ListAdapter เข้ามา เพื่อให้นักพัฒนาไม่ต้องมานั่งใส่คำสั่ง DiffUtil เองให้วุ่นวาย โดยมีหน้าตาประมาณนี้

// PostAdapter.kt

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

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PostViewHolder {
        return ...
    }

    override fun onBindViewHolder(holder: PostViewHolder, position: Int) {
        val post = getItem(position)
        holder.text = post.text
        holder.timestamp = post.timestamp
    }

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

        override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean {
            return oldItem.text == newItem.text && oldItem.timestamp == newItem.timestamp
        }
    }
}

        สำหรับ Adapter ที่ข้อมูลมีแค่ Type เดียวเท่านั้น ก็จะมีโค้ดแค่นี้จริงๆครับ คือหล่อมากอ่ะ จากเดิมที่ต้องสร้างทั้ง Adapter และ DiffUtil ก็สามารถนำมารวมด้วยกันแบบนี้ได้เลย เพราะโค้ดในนี้มันสั้นมากจริงๆ

        แต่ที่เจ๋งไปกว่านั้นคือตอนเรียกใช้งานนี่แหละ

// MainActivity.kt

class MainActivity : AppCompatActivity() {
    private lateinit var adapter: PostAdapter

    ...

    fun updatePostList(newPostList: List<Post>) {
        adapter.submitList(newPostList)
    }
}

        เวลาที่ข้อมูลมีการเปลี่ยนแปลงก็แค่โยนข้อมูลใหม่ทั้งหมดเข้าไปใน Adapter ด้วยคำสั่ง submitList(...) ได้เลย เดี๋ยวตัวมันจะจัดการกับข้อมูลให้เอง แล้วก็เช็คให้เองว่าข้อมูลเปลี่ยนแปลงอะไรบ้าง แล้ว Recycler View จะแสดง Animation เพื่ออัพเดทข้อมูลให้ผู้ใช้เห็นโดยอัตโนมัติ


        โคตรสบายอ่ะ

        สำหรับเงื่อนไขสำคัญของการใช้งาน ListAdapter ก็คือข้อมูลควรจะมี Unique ID เพื่อใช้ในการเปรียบเทียบใน DiffUtil รวมไปถึง Content ต่างๆที่นักพัฒนาต้องเขียนเข้าไปเอง

        ดังนั้นถ้าจะทำ View Holder หลายๆ Type ก็สามารถเขียนใน ListAdapter ได้เลยเช่นกัน แต่ทุกๆตัวจะต้องมี Unique ID เป็นของตัวเองด้วย ถึงแม้ว่าจะเป็น View Holder เปล่าๆที่เตรียมไว้ก็ต้องกำหนด ID นะ

// PostAdapter.kt

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

    override fun getItemViewType(position: Int): Int {
        return getItem(position).type
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when (viewType) {
        PostType.PHOTO -> PostViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.layout_photo_post_item, parent, false))
        else -> ...
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        if (holder is PhotoViewHolder) {
            val photoPost = getItem(position) as PhotoPost
            holder.load(photoPost.url)
            holder.text = photoPost.text
        }
        ...
    }

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

        override fun areContentsTheSame(oldItem: Post, newItem: Post): Boolean {
            if (oldItem is PhotoPost && newItem is PhotoPost) {
                return oldItem.url == newItem.url && oldItem.text == newItem.text && oldItem.timestamp == newItem.timestamp
            }
            ...
        }
    }
}

        และนอกจากนี้ใน DiffUtil ที่ปกติแล้วจะทำงานอยู่บน Main Thread แต่ถ้าเกิดว่ามีข้อมูลที่เยอะและซับซ้อนมาก ตอนที่ DiffUtil เปรียบเทียบข้อมูลแต่ละตัวเพื่อเช็คว่าตัวไหนถูกลบ ถูกแก้ไข หรือถูกเพิ่มเข้ามา ก็อาจจะทำให้แอปฯค้างไปชั่วขณะได้ ดังนั้น ListAdapter จึงมาพร้อมกับคลาสที่ชื่อว่า AsyncDifferConfig เพื่อครอบ DiffUtil ที่นักพัฒนาได้สร้างไว้ แล้วเดี๋ยวคำสั่งต่างๆใน DiffUtil ตัวนั้นๆจะทำงานอยู่บน Background Thread ทันที (เฮ้ย สะดวกไปแล้ววววว)

// PostAdapter.kt

class PostAdapter : ListAdapter<Post, RecyclerView.ViewHolder>(AsyncDifferConfig.Builder(PostDiffCallback()).build()) {
    ...
    class PostDiffCallback : DiffUtil.ItemCallback<Post>() {
        ...
    }
}

        อาจจะดูยาวไปหน่อย แต่เชื่อเถอะว่ามันดีกว่าการที่ต้องมานั่งเขียนให้มันทำงานบน Background Thread เองแน่นอน

สรุป

        ListAdapter ตัวใหม่ใน Support Library นี้จะช่วยให้นักพัฒนาลดขั้นตอนยุ่งยากในการอัพเดทข้อมูลและแสดงผลให้กับผู้ใช้ เหลือแค่ทำการ Binding ข้อมูลเข้ากับ ViewHolder และใส่ Logic ให้กับ DiffUtil นิดหน่อย ที่เหลือก็ปล่อยให้เป็นหน้าที่ของ ListAdapter ได้เลย

        และถ้าลองดูดีๆแล้วก็จะพบว่า ListAdapter นั้นมีความคล้ายกับ PagedListAdapter ใน Paging Library ของ Architecture Components เลย ซึ่งบอกเลยว่าใช่ครับ เพราะว่า PagedListAdapter นั้นต่อยอดจาก ListAdapter ในเรื่องของรูปแบบการดึงข้อมูลมาแสดงผล

        • ListAdapter จะเหมาะกับกรณีที่ได้ข้อมูลมาทีเดียวทั้งก้อนเลย
        • PagedListAdapter จะเหมาะกับกรณีที่ต้องดึงข้อมูลมาแสดงทีละส่วน

        ส่วนการทำงานอื่นๆก็เหมือนกันทั้งหมด ดังนั้นก็เลือกให้เหมาะสมกับงานของตัวเองด้วยล่ะ

        อยากเห็นโค้ดแบบเต็มๆก็ตามเข้าไปดูกันได้ที่ Android-ListAdapter [GitHub]

แหล่งข้อมูลอ้างอิง

        • ListAdapter [Android Developers]