ว่าด้วยเรื่อง RecyclerView กับการใช้งานจริงในแบบฉบับเจ้าของบล็อก ตอนที่ 1
หลังจากที่ RecyclerView ถูกเพิ่มเข้ามาใน Android 5.0 Lollipop และกลายเป็น Support Library ที่สามารถทำงานบนแอนดรอยด์เวอร์ชันต่ำกว่านั้นได้ จึงทำให้เจ้าของบล็อกได้ใช้ RecyclerView แทน ListView ไปโดยปริยาย เนื่องจากการใช้งานมันที่ถึงแม้จะเขียนเยอะแต่ก็ทำให้โค้ดออกมาเป็น Pattern ที่ถูกต้อง ไม่เหมือน ListView ที่ยังดูค่อนข้างสะเปะสะปะ จะเขียนยังไงก็ตามใจฉัน
บทความในซีรีย์เดียวกัน
จะว่าไปเจ้าของบล็อกก็ไม่ค่อยได้เขียนบทความเกี่ยวกับ RecyclerView ซักเท่าไรเนอะ บทความพื้นฐานเกี่ยวกับ RecyclerView ก็ไม่ได้เขียน เพราะว่ามีนักพัฒนาหลายๆคนเขียนบทความเกี่ยวกับเรื่องนี้แล้ว ถ้าผู้ที่หลงเข้ามาอ่านยังไม่เคยอ่านก็แนะนำให้ไปอ่านเพื่อทำความเข้าใจใน RecyclerView ก่อนนะครับ
- [โหมด Geek] RecyclerView สิ่งใหม่ที่กูเกิ้ลหวังว่าจะทำให้แอพฯแอนดรอยด์ดีขึ้น
- [ANDROID] มาลองทำ Multiple layout บน ListView/RecyclerView กัน
- [Dev] รู้จักกับ ViewHolder Pattern สำหรับ ListView, GridView และการมาของ RecyclerView
แล้วบทความนี้จะพูดถึง RecyclerView แบบไหนล่ะ?
ก็อย่างที่บอกในตอนแรกว่าเจ้าของบล็อกเลิกใช้ ListView และเปลี่ยนมาใช้ RecyclerView ในการทำงานแล้ว เพราะตัวมันมีความยืดหยุ่นในการใช้งานที่สูงกว่า ในขณะเดียวกันก็กำหนด Pattern ในการเขียนมาให้เลย จึงมั่นใจได้นิดนึงว่ารูปแบบโค้ดจะไม่เละเทะ
อะไรนะ? จะให้อธิบายวิธีการสร้าง RecyclerView แบบภาพข้างบนนี้น่ะหรอ? ไม่มีทางแน่นอน เพราะนี่มัน RecyclerView ธรรมดาเกินไป๊
บ่อยครั้งที่เจ้าของบล็อกต้องเขียนแอปฯที่มี Layout โหดร้ายพอสมควร ยกตัวอย่างว่าต้องดึงข้อมูลจาก Web Service แล้วเอามาแสดงในรูปแบบประมาณนี้
เมื่อพิจารณาจากข้อมูลจะเห็นว่ามีข้อมูลอยู่ 3 ส่วนด้วยกัน ซึ่งแต่ละส่วนก็จะมีข้อมูลแตกต่างกันออกไป และสุดท้ายก็ต้องทำออกมาให้เหมือนกับภาพตัวอย่างให้ได้อยู่ดี และเวลาเลื่อนขึ้นลงก็จะต้องเลื่อนพร้อมกันทั้งหมด ไม่ใช่เลื่อนแค่ส่วนที่เป็นข้อมูล Dynamic
ถ้าสังเกตดีๆก็จะเห็นว่าในภาพตัวอย่าง นอกจากส่วนที่เป็นข้อมูล ก็ยังมีส่วนอื่นๆประกอบอยู่ด้วยมีทั้งชื่อผู้ใช้งาน, หัวข้อของข้อมูลแต่ละชุด, สรุปรวมรายการทั้งหมด และปุ่มยืนยัน/ยกเลิกรายการ ซึ่งบางส่วนก็เป็นส่วนที่มีการ Fixed ข้อมูลไว้ และบางส่วนก็มีการเอาข้อมูลมาคำนวณออกมาเพื่อแสดงในช่องนั้นๆ
ผู้ที่หลงเข้ามาอ่านบางคนเห็นโจทย์แบบนี้ก็คงใช้วิธีจัด Layout ส่วนที่ Fixed ข้อมูลเตรียมไว้แล้วส่วนที่เป็นข้อมูลแบบ Dynamic ก็สร้าง Linear Layout ขึ้นมาเพื่อ Add View เพิ่มเข้าไปในทีหลัง โดยมี ScrollView ครอบอยู่ทั้งหน้าเพื่อให้เลื่อนขึ้นลงได้
ซึ่งวิธีดังกล่าวเป็นวิธีที่ไม่ค่อยถูกต้องซักเท่าไร เนื่องจากการใช้ ScrollView แล้วยัด View ทั้งหมดไว้ข้างในนั้น จะเกิดปัญหาเรื่องการ View ที่ยังกินทรัพยากรอยู่ เพราะ View ที่เก็บไว้ใน ScrollView ทั้งหมด จะไม่มีการ Recycle เกิดขึ้น
โดยปัญหาที่ว่าอาจจะไม่ค่อยเห็นผลถ้าข้อมูลมีจำนวนไม่เยอะมาก แต่ถ้าข้อมูลมีซัก 30 ชุดขึ้นไปก็คงไม่สนุกซักเท่าไร ตอน Add View เข้าไปพร้อมๆกันก็ทำให้ค้างชั่วขณะ และเวลาเลื่อนดูข้อมูลไปมาก็ไม่ลื่น
RecyclerView พระเอกของงานนี้
เพื่อแก้ปัญหาการใช้ ScrollView แล้วทำให้สิ้นเปลืองทรัพยากรให้กับ View ที่ยังไม่ได้แสดงโดยไม่จำเป็น จึงทำให้ RecyclerView เข้ามาแก้ปัญหานี้โดยเฉพาะ (ListView ก็ทำได้เหมือนกัน แต่เขียนยุ่งยากกว่า จึงไม่ค่อยแนะนำ)
แล้วจะเอา RecyclerView มาช่วยแก้ปัญหานี้ยังไงล่ะ? ก็ให้ทั้ง View ทุกตัวที่แสดงอยู่ในหน้านี้ เป็นส่วนหนึ่งของ RecyclerView ไปเลยสิ!!
เพื่อให้สามารถเลื่อนได้ทั้งหน้าจอและ View มีการ Recycle เมื่อเลื่อนออกนอกจอ ดังนั้นการใช้ RecyclerView ในงานแบบนี้จึงตอบโจทย์ได้ดีกว่าการใช้ ScrollView
แต่ความยากของการใช้ RecyclerView อยู่ที่ว่าจะทำยังไงให้ RecyclerView แสดงผลลัพธ์ออกมาตามที่ต้องการได้ต่างหากล่ะ
สำหรับผู้ที่หลงเข้ามาอ่านที่อยากจะเริ่มต้นไปพร้อมๆกับบทความนี้แบบง่ายๆ ขอแนะนำให้เปิดโปรเจคจาก Lovely Recycler View — Get Started [GitHub] ขึ้นมาเตรียมพร้อมได้เลย
กำหนดชื่อเรียกแต่ละส่วนให้เรียบร้อยก่อน
ก่อนจะเริ่มสร้าง RecyclerView แล้วเขียนโค้ด ผู้ที่หลงเข้ามาอ่านจะคิดก่อนว่า View ที่จะใช้แสดงใน RecyclerView ตัวนี้มีทั้งหมดกี่แบบ ซึ่งเจ้าของบล็อกแบ่งออกมาได้ทั้งหมด 9 แบบดังนี้
จะเห็นว่ามีกระทั่ง Empty View ด้วย เพื่อกำหนดเป็นพื้นที่ว่างเพื่อให้เกิดระยะห่างระหว่าง View ตัวอื่นๆ
เอาล่ะ มาเริ่มเตรียมโค้ดในแต่ละส่วนกันดีกว่า~
เปลี่ยน JSON ให้กลายเป็น Data Class
ขั้นตอนนี้คงไม่อธิบายว่าจะแปลงจาก JSON ยังไง เพราะส่วนใหญ่ก็คงจะใช้ GSON ในการแปลงกันอยู่แล้ว แต่จาก JSON ในตัวอย่าง ก็จะต้องสร้าง Data Class แบบนี้
// OrderDetail.kt
@Keep
@Parcelize
data class OrderDetail(
@SerializedName("food_list")
val foodList: MutableList<Food>,
@SerializedName("book_list")
val bookList: MutableList<Book>,
@SerializedName("music_list")
val musicList: MutableList<Music>,
) : Parcelable
// Food.kt
@Keep
@Parcelize
data class Food(
@SerializedName("order_name")
val orderName: String,
val amount: Int,
val price: Int,
) : Parcelable
// Book.kt
@Keep
@Parcelize
data class Book(
@SerializedName("ISBN")
val isbn: String,
@SerializedName("book_name")
val bookName: String,
val author: String,
val publishDate: String,
val publication: String,
val price: Int,
val pages: Int,
) : Parcelable
// Music.kt
@Keep
@Parcelize
data class Music(
val artist: String,
val album: String,
@SerializedName("release_date")
val releaseDate: String,
val track: Int,
val price: Int
) : Parcelable
สำหรับ@SerializedName
จะเป็น Annotation ของ GSON เพื่อช่วยแปลงข้อมูล JSON ให้กลายเป็นตัวแปรที่ต้องการ ส่วน@Keep
เอาไว้บอกให้ Proguard ข้ามคลาสเหล่านี้ไป และ@Parcelize
สำหรับช่วยทำให้ Data Class กลายเป็น Parcelable แบบง่ายๆ
ดังนั้นข้อมูลที่ได้มาเป็น JSON จะถูกแปลงให้กลายเป็นคลาส OrderDetail เพื่อเอาไปใช้งานนั่นเอง
กำหนด Type สำหรับ ViewHolder
ประกาศ Type ของ View ทั้ง 9 แบบให้ครบ โดยแต่ละ Type จะกำกับด้วยตัวเลขที่ต่างกันออกไป (ระวังอย่าให้เลขซ้ำกัน)
// OrderDetailType.kt
object OrderDetailType {
const val TYPE_EMPTY = 0
const val TYPE_USER_DETAIL = 1
const val TYPE_TITLE = 2
const val TYPE_SECTION = 3
const val TYPE_ORDER = 4
const val TYPE_SUMMARY = 5
const val TYPE_TOTAL = 6
const val TYPE_NOTICE = 7
const val TYPE_BUTTON = 8
}
ซึ่ง Type ตรงนี้ก็คือ Type ที่จะใช้อ้างอิงใน RecyclerView นั่นเอง
เตรียม Resource ให้พร้อม
Color Resource
ตั้งชื่อสีให้เวอร์ๆไว้ก่อน เหมือนที่มือถือหลายๆยี่ห้อชอบทำกัน
<!-- colors.xml -->
<resources>
<!-- ... -->
<color name="funny_dark_pink">#d94e6d</color>
<color name="soft_red">#ef4c43</color>
<color name="seasonal_orange">#ffc73c</color>
<color name="dark_seasonal_orange">#efb82e</color>
<color name="natural_green">#8bc860</color>
<color name="honest_green">#48bd89</color>
<color name="dark_honest_green">#3cab7a</color>
<color name="sky_light_blue">#79c7dd</color>
<color name="supreme_blue">#0e76bd</color>
<color name="jet_black">#222222</color>
<color name="space_gray">#949494</color>
<color name="little_light_gray">#f3f3f3</color>
<color name="angel_white">#ffffff</color>
</resources>
Drawable Resource
สำหรับรูปไอคอน User ก็ใช้ภาพจาก Google Material Design Icon เพื่อความสะดวกรวดเร็ว
ส่วนปุ่มก็สร้างขึ้นมาด้วย Shape Drawable แบบง่ายๆ แล้วทำเป็น Selector นิดหน่อย (มีแค่ Normal State กับ Pressed State เท่านั้น ขี้เกียจทำ Disable State กับ Focused State)
Dimension Resource
ขนาดต่างๆที่จะต้องใช้ใน Layout XML
<!-- dimens.xml -->
<resources>
<!-- ... -->
<dimen name="text_size_extra_small">10sp</dimen>
<dimen name="text_size_small">12sp</dimen>
<dimen name="text_size">16sp</dimen>
<dimen name="text_size_large">20sp</dimen>
<dimen name="text_size_extra_large">24sp</dimen>
<dimen name="margin_padding_small">4dp</dimen>
<dimen name="margin_padding">8dp</dimen>
<dimen name="margin_padding_large">16dp</dimen>
<dimen name="margin_padding_extra_large">24dp</dimen>
<dimen name="min_button_width">120dp</dimen>
<dimen name="empty_space_height">16dp</dimen>
<dimen name="user_icon_size">30dp</dimen>
</resources>
String Resource
ในตัวอย่างนี้ก็จะมีแค่ข้อความเพียงชุดเดียวเท่านั้น แต่ถึงกระนั้นก็อย่าขี้เกียจสร้างเพียงเพราะว่ามันมีแค่ตัวเดียวนะ พยายามสร้างเป็น String Resource ตลอดเวลา เพื่อที่ว่าเวลาจะมาแก้ไขหรือเพิ่มภาษาอื่นๆจะได้ไม่ต้องมาเสียเวลาทำเป็น String Resource ทีหลัง
<!-- strings.xml -->
<resources>
<!-- ... -->
<string name="cancel">Cancel</string>
<string name="confirm">Confirm</string>
<string name="total">Total</string>
<string name="order_notice">* After confirming your order can not be undone</string>
<string name="your_order">Your Order</string>
<string name="summary">Summary</string>
<string name="book">Book</string>
<string name="food">Food</string>
<string name="music">Music</string>
<string name="baht_unit">฿</string>
</resources>
เอาล่ะ ตอนนี้ Resource ต่างๆเตรียมพร้อมแล้ว มาสร้าง ViewHolder ให้กับ Adapter กันต่อเลย
สร้าง Adapter เปล่าๆเตรียมไว้
เนื่องจากมีบางอย่างที่จะต้องสร้างเพื่อใช้งานใน Adapter แต่เพื่อให้ Adapter พร้อมใช้งานชั่วคราวระหว่างสร้างบางอย่างที่ว่า ดังนั้นตอนนี้จึงขอสร้างโครง Adapter เปล่าๆเตรียมไว้ก่อน
// OrderAdapter.kt
class OrderAdapter : RecyclerView.Adapter<ViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
return /* ... */
}
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
}
override fun getItemCount(): Int = 0
}
ประมาณนี้ก็พอ เดี๋ยวค่อยมาเพิ่มโค้ดอย่างอื่นทีหลัง
มามะ สร้าง Data Class สำหรับ ViewHolder กันต่อ
มาถึงตรงนี้ ผู้ที่หลงเข้ามาอ่านอาจจะสงสัยและแปลกใจว่าทำไมเจ้าของบล็อกถึงสร้าง Data Class สำหรับ ViewHolder ด้วยล่ะ ทำไมไม่ใช้ OrderDetail ไปเลย
ทั้งนี้ก็เพราะว่า OrderDetail ยังเป็นข้อมูลดิบอยู่ ที่ยังไม่ผ่านการกรองข้อมูลที่ไม่จำเป็น และยังไม่ได้จัดเรียงข้อมูลใหม่
ซึ่งการโยนคลาส OrderDetail เข้ามาใน Adapter โดยตรงนั้นจะทำให้เจ้าของบล็อกต้องไปเขียน Logic ดังกล่าวอยู่ใน Adapter ไปด้วย ซึ่งคงไม่ค่อยโอเคเท่าไร เพราะจะทำให้ Adapter ตัวนั้นมี Logic ข้างในที่ซับซ้อนมากขึ้นไปอีก
ดังนั้นเจ้าของบล็อกจึงต้องสร้าง Data Class สำหรับ Adapter ขึ้นมาอีกชุด เพื่อให้ข้อมูลใน Adapter อยู่ในรูปแบบที่กระชับและดูได้ง่ายที่สุดเท่าที่เป็นไปได้ โดยแปลงข้อมูลจาก OrderDetail อีกที
สร้าง Base Class สำหรับ Data Class ก่อน
เพื่อให้ข้อมูลที่ส่งให้กับ ViewHolder แต่ละตัวสามารถแยกกันได้ง่าย ควรสร้าง Sealed Class ขึ้นมาเพื่อเก็บประเภทของข้อมูลแต่ละตัวว่าเป็นข้อมูลประเภทไหน เนื่องจากการทำ Multiple View Type ใน Adapter ของ RecyclerView จะแยกด้วยค่า Integer
ดังนั้นควรสร้าง Sealed Class ที่เก็บค่า type
เป็น Integer เตรียมไว้ด้วย
// OrderDetailItem.kt
sealed class OrderDetailItem(val type: Int): Parcelable
จากนั้นก็ให้สร้าง Data Class และ Object สำหรับ ViewHolder จาก Sealed Class ตัวนี้ให้ครบทั้งหมดซะ
การสร้าง Data Class หรือ Object ที่มาจาก Sealed Class จะต้องสร้างไว้ข้างใน Sealed Class เท่านั้น
User Detail
เก็บชื่อของผู้ใช้
@Parcelize
data class UserDetail(
val name: String
) : OrderDetailItem(OrderDetailType.TYPE_USER_DETAIL)
จะเห็นว่ามีการกำหนดค่าให้กับ type
เป็น TYPE_USER_DETAIL
ไว้เลย ซึ่งคลาสอื่นๆก็จะกำหนดค่าที่แตกต่างกันออกไป
Title
เก็บแค่ชื่อ Title
@Parcelize
data class Title(
val title: String
) : OrderDetailItem(OrderDetailType.TYPE_TITLE)
Section
เก็บข้อมูลชื่อหัวข้อของสินค้าแต่ละกลุ่ม (Section)
@Parcelize
data class Section(
val section: String
) : OrderDetailItem(OrderDetailType.TYPE_SECTION)
Order
เก็บข้อมูลรายการสินค้า ซึ่งจะแสดงข้อมูล 3 อย่างด้วยกัน
- ช่ือสินค้า
- ข้อมูลเพิ่มเติมของสินค้า
- ราคา
@Parcelize
data class Order(
val name: String,
val detail: String,
val price: String
) : OrderDetailItem(OrderDetailType.TYPE_ORDER)
Summary
เก็บประเภทของสินค้าและราคารวมทั้งหมดของสินค้าประเภทนั้นๆ
@Parcelize
data class Summary(
val name: String,
val price: String
) : OrderDetailItem(OrderDetailType.TYPE_SUMMARY)
Total
เก็บราคารวมของสินค้าทั้งหมด
@Parcelize
data class Total(
val totalPrice: String
) : OrderDetailItem(OrderDetailType.TYPE_TOTAL)
Notice
ไม่ได้มีหน้าที่เก็บข้อมูลใดๆ แค่มีไว้บอกว่าเป็น Notice Type เท่านั้น
@Parcelize
object Notice : OrderDetailItem(OrderDetailType.TYPE_NOTICE)
เนื่องจาก Notice ไม่ได้เก็บข้อมูลใดๆไว้ จึงต้องสร้างเป็น Object แทน
Button
บอกแค่ว่าเป็น Button Type เท่านั้น
@Parcelize
object Button : OrderDetailItem(OrderDetailType.TYPE_BUTTON)
Empty
ทำหน้าที่บอกแค่ว่าเป็น Empty Type เท่านั้น
@Parcelize
object Empty : OrderDetailItem(OrderDetailType.TYPE_EMPTY)
สร้าง ViewHolder และ Layout Resource สำหรับ View ทุกแบบ
ก่อนจะสร้าง Adapter ก็ควรจะสร้าง ViewHolder ให้พร้อมเสียก่อนเนอะ ทั้ง Layout Resource และ ViewHolder สำหรับ View ทั้ง 9 แบบเลย
และ ViewHolder ตัวไหนที่จำเป็นต้องกำหนดข้อมูลเพิ่มเข้าไปด้วย ก็จะเพิ่มคำสั่ง bind(...)
พร้อมกับคำสั่งสำหรับนำข้อมูลไปแสดงใน View ที่ต้องการ
User Detail
<!-- view_user_detail.xml -->
<LinearLayout ... >
<ImageView ... />
<TextView
android:id="@+id/tv_user_name"
... />
</LinearLayout>
// UserDetailViewHolder.kt
class UserDetailViewHolder(private val binding: ViewUserDetailBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: OrderDetailItem.UserDetail) {
binding.tvUserName.text = item.name
}
}
Title
<!-- view_title.xml -->
<LinearLayout ... >
<TextView
android:id="@+id/tv_title"
... />
</LinearLayout>
// TitleViewHolder.kt
class TitleViewHolder(private val binding: ViewTitleBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: OrderDetailItem.Title) {
binding.tvTitle.text = item.title
}
}
Section
<!-- view_section.xml -->
<LinearLayout ... >
<TextView
android:id="@+id/tv_section"
... />
</LinearLayout>
// SectionViewHolder.kt
class SectionViewHolder(private val binding: ViewSectionBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: OrderDetailItem.Section) {
binding.tvSection.text = item.section
}
}
Order
<!-- view_order.xml -->
<LinearLayout ... >
<LinearLayout ... >
<TextView
android:id="@+id/tv_order_name"
... />
<TextView
android:id="@+id/tv_order_description"
... />
</LinearLayout>
<TextView
android:id="@+id/tv_order_price"
... />
</LinearLayout>
// OrderViewHolder.kt
class OrderViewHolder(private val binding: ViewOrderBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: OrderDetailItem.Order) {
binding.tvOrderName.text = item.name
binding.tvOrderDetail.text = item.detail
binding.tvOrderPrice.text = item.price
}
}
Summary
<!-- view_summary.xml -->
<LinearLayout ... >
<TextView
android:id="@+id/tv_summary_name"
... />
<TextView
android:id="@+id/tv_summary_price"
... />
</LinearLayout>
// SummaryViewHolder.kt
class SummaryViewHolder(private val binding: ViewSummaryBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: OrderDetailItem.Summary) {
binding.tvSummaryName.text = item.name
binding.tvSummaryPrice.text = item.price
}
}
Total
<!-- view_total.xml -->
<LinearLayout ... >
<TextView ... />
<TextView
android:id="@+id/tv_total_price"
... />
</LinearLayout>
// TotalViewHolder.kt
class TotalViewHolder(private val binding: ViewTotalBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(item: OrderDetailItem.Total) {
binding.tvTotalPrice.text = item.totalPrice
}
}
Notice
<!-- view_notice.xml -->
<LinearLayout ... >
<TextView ... />
</LinearLayout>
// NoticeViewHolder.kt
class NoticeViewHolder(binding: ViewNoticeBinding) : RecyclerView.ViewHolder(binding.root)
Button
<!-- view_button.xml -->
<LinearLayout ... >
<Button
android:id="@+id/btn_negative"
... />
<Button
android:id="@+id/btn_positive"
... />
</LinearLayout>
// ButtonViewHolder.kt
class ButtonViewHolder(private val binding: ViewButtonBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(
onPositiveClicked: () -> Unit,
onNegativeClicked: () -> Unit
) {
binding.btnPositive.setOnClickListener { onPositiveClicked.invoke() }
binding.btnNegative.setOnClickListener { onNegativeClicked.invoke() }
}
}
ถึงแม้ ButtonViewHolder จะไม่มีข้อมูลสำหรับแสดงผล แต่ว่าต้องส่ง Click Event ออกไปเพื่อให้ Activity หรือ Fragment ที่เรียกใช้งาน RecyclerView รับรู้ได้ว่าผู้ใช้ทำการกด Positive Button หรือ Negative Button เพื่อทำคำสั่งอย่างอื่นต่อไป จึงมีการรับ Function เค้ามาเพื่อทำเป็น Event Listener
Empty
<!-- view_empty.xml -->
<FrameLayout ... />
// EmptyViewHolder
class EmptyViewHolder(binding: ViewEmptyBinding) : RecyclerView.ViewHolder(binding.root)
เย้ เตรียมเสร็จทั้งหมดซะที
ต่อไปก็เหลือ Converter แล้ว
แต่เดี๋ยวก่อนนนน เจ้าของบล็อกรู้สึกว่าบทความเริ่มจะยาวเกินไปแล้วล่ะ และเนื้อหาต่อจากนี้ก็ยาวพอสมควร ดังนั้นขอแยกเป็นบทความหลายตอนแทนดีกว่าเนอะ
สำหรับผู้ที่หลงเข้ามาอ่านคนใดที่อยากจะดูโค้ดในตอนที่ 3 นี้ ก็สามารถไปดาวน์โหลดได้ที่ Lovely Recycler View — Part 1 Ending [Github]
ตามไปอ่าน ตอนที่ 2 แทนแล้วกันเนอะ