สร้าง RecyclerView แบบสบายๆด้วย ListAdapter
วันนี้แอบไปส่อง RecyclerView เล่นๆแล้วก็ได้พบเจอกับ ListAdapter ที่เอาไว้ใช้กับ RecyclerView และมีการทำงานที่น่าสนใจจนต้องหยิบเอามาเล่าให้ฟังกันฮะ
Adapter ของ RecyclerView แบบเดิมๆ
โดยปกติแล้วเวลาจะสร้าง 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 นั่นเอง
เพื่อให้สะดวกสบายมากขึ้นจึงมีการสร้าง 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(...)
ได้เลย เดี๋ยวตัวมันจะจัดการกับข้อมูลให้เอง แล้วก็เช็คให้เองว่าข้อมูลเปลี่ยนแปลงอะไรบ้าง แล้ว RecyclerView จะแสดง Animation เพื่ออัพเดทข้อมูลให้ผู้ใช้เห็นโดยอัตโนมัติ
โคตรสบายอ่ะ
สำหรับเงื่อนไขสำคัญของการใช้งาน ListAdapter ก็คือข้อมูลควรจะมี Unique ID เพื่อใช้ในการเปรียบเทียบใน DiffUtil รวมไปถึง Content ต่างๆที่นักพัฒนาต้องเขียนเข้าไปเอง
ดังนั้นถ้าจะทำ ViewHolder หลายๆ Type ก็สามารถเขียนใน ListAdapter ได้เลยเช่นกัน แต่ทุกๆตัวจะต้องมี Unique ID เป็นของตัวเองด้วย ถึงแม้ว่าจะเป็น ViewHolder เปล่าๆที่เตรียมไว้ก็ต้องกำหนด 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 จะช่วยให้นักพัฒนาลดขั้นตอนยุ่งยากในการอัพเดทข้อมูลและแสดงผลให้กับผู้ใช้ เหลือแค่ทำการ Binding ข้อมูลเข้ากับ ViewHolder และใส่ Logic ให้กับ DiffUtil นิดหน่อย ที่เหลือก็ปล่อยให้เป็นหน้าที่ของ ListAdapter ได้เลย
และถ้าลองดูดีๆแล้วก็จะพบว่า ListAdapter นั้นมีความคล้ายกับ PagedListAdapter ใน Paging Library ของ Android Jetpack ก็ต้องบอกเลยว่าใช่ครับ เพราะว่า PagedListAdapter นั้นต่อยอดจาก ListAdapter ในเรื่องของรูปแบบการดึงข้อมูลมาแสดงผล
- ListAdapter จะเหมาะกับกรณีที่ได้ข้อมูลมาทีเดียวทั้งก้อนเลย
- PagedListAdapter จะเหมาะกับกรณีที่ต้องดึงข้อมูลมาแสดงทีละส่วน
ส่วนการทำงานอื่นๆก็เหมือนกันทั้งหมด ดังนั้นก็เลือกให้เหมาะสมกับงานของตัวเองด้วยล่ะ
อยากเห็นโค้ดแบบเต็มๆก็ตามเข้าไปดูกันได้ที่ RecyclerView-ListAdapter [GitHub]