กลับมากันต่อกับบทความที่จะพาผู้ที่หลงเข้ามาอ่านไปดูกับวิธีการใช้งาน RecyclerView กับรูปแบบการแสดงผลที่หลากหลายและซับซ้อนในแบบฉบับของเจ้าของบล็อก

ถ้าผู้ที่หลงเข้ามาอ่านคนไหนยังไม่ได้อ่านตอนที่ 1 มาก่อน โปรดกลับไปอ่านก่อนนะครับ เพราะเป็นเนื้อหาต่อเนื่องกัน

บทความในซีรีย์เดียวกัน

ในตอนที่ 1 เจ้าของบล็อกได้จบลงหลังจากสร้าง Data Class และ ViewHolder เสร็จเรียบร้อยแล้ว ดังนั้นในตอนที่ 2 นี้ก็จะเริ่มกันที่ Converter กันต่อเลย

ผู้ที่หลงเข้ามาอ่านสามารถดาวน์โหลดตัวอย่างได้จาก Lovely Recycler View — Part 1 Ending [GitHub] เพื่อความต่อเนื่องครับ

akexorcist/LovelyRecyclerView
[Android] RecyclerView with complex layout and data - akexorcist/LovelyRecyclerView

ก่อนจะเริ่มสร้าง Converter อยากให้รู้ก่อนว่า เจ้าของบล็อกสร้างคลาสตัวหนึ่งเพื่อจำลองว่ามันคือคลาสสำหรับดึงข้อมูลจาก Server ละกันเนอะ ไม่ต้องสนใจข้างในคลาสดังกล่าว เจ้าของบล็อกเขียนลวกๆไว้ โดยเอา JSON ที่เตรียมไว้เพื่อแปลงให้กลายเป็น OrderDetail แล้วส่งกลับมา

private fun loadOrderDetail() {
    FakeNetwork.getFakeOrderDetail { orderDetail ->
        this.orderDetail = orderDetail
        val convertedItems = convertToOrderDetailItems(orderDetail)
        updateOrderDetailItems(convertedItems)
    }
}

ซึ่งผลลัพธ์ที่ได้จะต้องแปลงข้อมูลแล้วส่งให้ Adapter เพื่อแสดงผลใน RecyclerView

Converter ที่จะแปลงข้อมูลให้เรียบง่ายมากขึ้น

อย่างที่บอกในตอนแรกว่าการเอาข้อมูลดิบไปใส่ไว้ใน Adapter โดยตรงคงไม่ใช่เรื่องสนุกซักเท่าไร โดยเฉพาะโค้ดที่ต้องมีการดูแลกันในระยะยาว

ดังนั้นควรจะมี Converter ที่ทำหน้าที่แปลงข้อมูลดิบที่อยู่ในรูปของคลาสให้กลายเป็น OrderDetailItem ที่เป็น List หรือ Array เพื่อส่งให้ Adapter แล้วนำไปแสดงผลได้เลยไม่ต้องคิดหรือคำนวณอีกแล้ว

ทีนี้ขอย้อนความทรงจำก่อนว่า RecyclerView ที่ลูกค้าต้องการเนี่ย มันจะมีส่วนของข้อมูลที่ Fixed ตำแหน่งไว้ และข้อมูลอีกส่วนคือข้อมูล Dynamic ที่มีจำนวนไม่แน่นอน ซึ่งมันดันอยู่แทรกกันไปมาแบบนี้

พอลองแบ่งว่า View Holder หรือ Layout แบบไหนที่แสดงเป็น Fixed หรือ Dynamic

เมื่อเห็นข้อมูลที่ Dynamic แบบนี้ให้เดาไว้เลยว่า มันจะต้องใช้วิธี For-Loop แน่นอน และ Converter ของเจ้าของบล็อกก็จะต้องแปลงข้อมูลออกมาเป็น Section, Order และ Summary ให้ได้

โดยเริ่มจากออกแบบหน้าตาของคำสั่งที่อยากจะได้ก่อน ซึ่งเจ้าของบล็อกอยากจะให้มันออกมาประมาณนี้

val orderDetailItems = mutableListOf<OrderDetailItem>().apply {
    add(OrderDetailConverter.createUserDetail(/* ... */))
    add(OrderDetailConverter.createTitle(/* ... */))
    addAll(OrderDetailConverter.createSectionAndOrder(/* ... */))
    add(OrderDetailConverter.createTitle(/* ... */))
    addAll(OrderDetailConverter.createSummary(/* ... */))
    add(OrderDetailConverter.createTotal(/* ... */))
    add(OrderDetailConverter.createNotice())
    add(OrderDetailConverter.createButton())
}

จะเห็นว่า Converter นั้นประกอบไปด้วยหลาย Method ที่เอาไว้สร้าง ViewHolder ที่แตกต่างกันออกไป ตัวที่เป็นแบบ Fixed ก็จะสร้างขึ้นมาแล้วใช้คำสั่ง add(...) เพิ่มเข้าไปใน List ตรงๆเลย แต่ถ้าเป็น Dynamic ก็จะใช้ addAll(...) แทน เพราะข้อมูลมีได้มากกว่า 1 ตัว

เริ่มสร้าง Converter ได้เลย

เมื่อกำหนดรูปแบบที่ต้องการแล้ว ก็สร้างขึ้นมาได้เลย แต่ตอนนี้ขอสร้างขึ้นมาแบบยังไม่ใส่คำสั่งอะไรข้างในก่อนนะ

// OrderDetailConverter.kt
object OrderDetailConverter {
    fun createUserDetail(/* ... */): OrderDetailItem.UserDetail {
        return /* ... */
    }

    fun createTitle(/* ... */): OrderDetailItem.Title {
        return /* ... */
    }

    fun createTotal(/* ... */): OrderDetailItem.Total {
        return /* ... */
    }

    fun createNotice(): OrderDetailItem.Notice {
        return /* ... */
    }

    fun createButton(): OrderDetailItem.Button {
        return /* ... */
    }

    fun createEmpty(): OrderDetailItem.Empty {
        return /* ... */
    }

    fun createSectionAndOrder(/* ... */): List<OrderDetailItem> {
        return /* ... */
    }

    fun createSummary(/* ... */): List<OrderDetailItem.Summary> {
        return /* ... */
    }
}

จากนั้นก็ทยอยทำทีละคำสั่งจนครบไป

คำสั่ง createUserDetail

การสร้าง UserDetail จะต้องมี String ที่เป็นชื่อของผู้ใช้ด้วย ดังนั้นคำสั่งจะออกมาในลักษณะง่ายๆแบบนี้

fun createUserDetail(name: String): OrderDetailItem.UserDetail {
    return OrderDetailItem.UserDetail(
        name = name
    )
}

คำสั่ง createTitle

การสร้าง Title จะใช้ String ที่เป็นชื่อของ Order แต่ละประเภท

fun createTitle(title: String): OrderDetailItem.Title {
    return OrderDetailItem.Title(
        title = title
    )
}

คำสั่ง createTotal

การสร้าง Total จะต้องส่ง OrderDetail เข้ามาด้วย เพราะต้องรวมราคาจากรายการทั้งหมด และ String สำหรับหน่วยเงินที่จะใช้แสดงผล

fun createTotal(orderDetail: OrderDetail, currency: String): OrderDetailItem.Total {
    val total = (orderDetail.foodList?.sumOf { it.price } ?: 0) +
            (orderDetail.bookList?.sumOf { it.price } ?: 0) +
            (orderDetail.musicList?.sumOf { it.price } ?: 0)
    return OrderDetailItem.Total(
        totalPrice = "$total$currency",
    )
}

คำสั่ง createNotice

สำหรับ Notice ไม่ต้องกำหนดข้อมูลอะไร จึงสามารถสร้างขึ้นมาได้เลย

fun createNotice(): OrderDetailItem.Notice {
    return OrderDetailItem.Notice
}

คำสั่ง createButton

สำหรับ Button ก็ไม่ต้องกำหนดข้อมูลอะไรเช่นกัน

fun createButton(): OrderDetailItem.Button {
    return OrderDetailItem.Button
}

คำสั่ง createEmpty

สำหรับ Empty ก็ไม่ต้องกำหนดข้อมูลเหมือนกัน

fun createEmpty(): OrderDetailItem.Empty {
    return OrderDetailItem.Empty
}

เอาล่ะ ตอนนี้สร้าง Converter ของ ViewHolder ที่เป็นแบบ Fixed กันแล้ว ซึ่งคำสั่งจะค่อนข้างเข้าใจได้ไม่ยาก ทีนี้มาดูกันต่อกับ Conveter สำหรับ ViewHolder ที่เป็นแบบ Dynamic

คำสั่ง createSectionAndOrder

เนื่องจาก Section และ Order เป็นของที่ต้องสร้างคู่กัน จึงรวมไว้ใน Method เดียวกันเลย เพื่อสร้างเป็น List ของทั้งหมดออกมาในทีเดียว

fun createSectionAndOrder(
    orderDetail: OrderDetail,
    foodTitle: String,
    bookTitle: String,
    musicTitle: String,
    currency: String
): List<OrderDetailItem> {
    return mutableListOf<OrderDetailItem>().apply {
        // Food
        if (!orderDetail.foodList.isNullOrEmpty()) {
            add(
                OrderDetailItem.Section(
                    section = foodTitle
                )
            )
            addAll(
                orderDetail.foodList.map { food ->
                    OrderDetailItem.Order(
                        name = food.orderName,
                        detail = "x${food.amount}",
                        price = "${food.price}$currency"
                    )
                })
        }

        // Book
        if (!orderDetail.bookList.isNullOrEmpty()) {
            add(
                OrderDetailItem.Section(
                    section = bookTitle
                )
            )
            addAll(
                orderDetail.bookList.map { book ->
                    OrderDetailItem.Order(
                        name = book.bookName,
                        detail = book.author,
                        price = "${book.price}$currency"
                    )
                })
        }

        // Music
        if (!orderDetail.musicList.isNullOrEmpty()) {
            add(
                OrderDetailItem.Section(
                    section = musicTitle
                )
            )
            addAll(
                orderDetail.musicList.map { music ->
                    OrderDetailItem.Order(
                        name = music.album,
                        detail = music.artist,
                        price = "${music.price}$currency"
                    )
                })
        }
    }
}

คำสั่งข้างในจะยาวเป็นพิเศษหน่อย เพราะว่ารวมข้อมูลทั้ง Food, Book และ Music ไว้ในนั้นทีเดียวเลย ถ้าอยากจะสร้าง Method สำหรับแต่ละชุดก็ได้เช่นกัน

จะเห็นว่าข้อมูลที่ 3 ประเภทนั้นเป็น Data Class ที่แตกต่างกัน แต่เจ้าของบล็อกทำให้ข้อมูลเหล่านี้แสดงข้อมูลในรูปของคลาส Order เหมือนกัน เพราะแสดงข้อมูลในรูปแบบที่เหมือนๆกัน

คำสั่ง createSummary

การสร้าง Summary จะต้องรวมราคาแยกกันในแต่ละชุด จึงมีการส่ง OrderDetail เข้ามาเพื่อใช้คำนวณและสร้างเป็น Summary แบบ List

fun createSummary(
    orderDetail: OrderDetail,
    foodTitle: String,
    bookTitle: String,
    musicTitle: String,
    currency: String
): List<OrderDetailItem.Summary> {
    return mutableListOf<OrderDetailItem.Summary>().apply {
        // Food
        if (!orderDetail.foodList.isNullOrEmpty()) {
            add(
                OrderDetailItem.Summary(
                    name = foodTitle,
                    price = "${orderDetail.foodList.sumOf { it.price }}$currency"
                )
            )
        }

        // Book
        if (!orderDetail.bookList.isNullOrEmpty()) {
            add(
                OrderDetailItem.Summary(
                    name = bookTitle,
                    price = "${orderDetail.bookList.sumOf { it.price }}$currency"
                )
            )
        }

        // Music
        if (!orderDetail.musicList.isNullOrEmpty()) {
            add(
                OrderDetailItem.Summary(
                    name = musicTitle,
                    price = "${orderDetail.musicList.sumOf { it.price }}$currency"
                )
            )
        }
    }
}

เอาล่ะ ตอนนี้ OrderConverter ของเจ้าของบล็อกก็พร้อมใช้งานแล้ว~

สร้าง Activity, เตรียม RecyclerView ให้พร้อม

เจ้าของบล็อกก็ไปสร้าง Activity ขึ้นมาโดยมี RecyclerView อยู่ข้างในเพื่อใช้สำหรับแสดงข้อมูลรายการสินค้า ซึ่งตรงนี้ไม่น่าจะมีอะไรต้องอธิบายเนอะ

<!-- activity_main.xml -->
<androidx.constraintlayout.widget.ConstraintLayout ... >

    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/rv_order_detail"
        ... />
</androidx.constraintlayout.widget.ConstraintLayout>

สำหรับ MainActivity เจ้าของบล็อกจะเตรียมคำสั่งไว้แบบนี้

// MainActivity.kt
class MainActivity : AppCompatActivity() {
    private val binding: ActivityMainBinding by lazy { ActivityMainBinding.inflate(layoutInflater) }
    private lateinit var adapter: OrderDetailAdapter
    private var orderDetail: OrderDetail? = null

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)
        setupView()
        loadOrderDetail()
    }

    private fun setupView() {
        adapter = OrderDetailAdapter(/* ... */)
        binding.rvOrderDetail.layoutManager = LinearLayoutManager(this)
        binding.rvOrderDetail.adapter = adapter
    }

    private fun loadOrderDetail() {
        FakeNetwork.getFakeOrderDetail { orderDetail ->
            this.orderDetail = orderDetail
            val convertedItems = convertToOrderDetailItems(orderDetail)
            updateOrderDetailItems(convertedItems)
        }
    }

    private fun convertToOrderDetailItems(orderDetail: OrderDetail): List<OrderDetailItem> {
        val name = "Sleeping For Less"
        val title = getString(R.string.your_order)
        val summaryTitle = getString(R.string.summary)

        val foodTitle = getString(R.string.food)
        val bookTitle = getString(R.string.book)
        val musicTitle = getString(R.string.music)
        val currency = getString(R.string.baht_unit)

        return mutableListOf<OrderDetailItem>().apply {
            add(OrderDetailConverter.createUserDetail(name))
            add(OrderDetailConverter.createTitle(title))
            addAll(OrderDetailConverter.createSectionAndOrder(orderDetail, foodTitle, bookTitle, musicTitle, currency))
            add(OrderDetailConverter.createTitle(summaryTitle))
            addAll(OrderDetailConverter.createSummary(orderDetail, foodTitle, bookTitle, musicTitle, currency))
            add(OrderDetailConverter.createTotal(orderDetail, currency))
            add(OrderDetailConverter.createNotice())
            add(OrderDetailConverter.createButton())
        }
    }

    private fun updateOrderDetailItems(newItems: List<OrderDetailItem>) {
        // adapter.orderDetailItems = newItems
        // adapter.notifyDataSetChanged()
    }
}

หัวใจสำคัญของการทำงานอยู่ที่ loadOrderDetail() ที่จะเริ่มทำงานทันทีที่ Activity เริ่มทำงาน กับ convertToOrderDetailItems(...) ที่จะแปลงข้อมูลที่ได้เพื่อแสดงบน RecyclerView

กลับไปที่ Adapter ที่สร้างเตรียมไว้ในตอนแรก

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

เผื่อผู้ที่หลงเข้ามาอ่านคนใดลืมไปแล้ว หยิบมาให้ดูอีกครั้งก่อนจะเพิ่มโค้ดเข้าไปละกัน

// OrderDetailAdapter.kt
class OrderDetailAdapter : RecyclerView.Adapter<ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
        return /* ... */
    }

    override fun onBindViewHolder(holder: ViewHolder, position: Int) {
    }
    
    override fun getItemCount(): Int = 0
}

เอาล่ะ ทีนี้ลองเพิ่มโค้ดที่สำคัญเข้าไปนิดหน่อยก่อน

// OrderDetailAdapter.kt
class OrderDetailAdapter(
    private val onPositiveButtonClicked: () -> Unit,
    private val onNegativeButtonClicked: () -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    var orderDetailItems: List<OrderDetailItem> = listOf()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
        return /* .. */
    }

    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        /* ... */
    }

    override fun getItemViewType(position: Int): Int {
        return orderDetailItems.getOrNull(position)?.type ?: OrderDetailType.TYPE_EMPTY
    }

    override fun getItemCount(): Int = orderDetailItems.size
}

โดยจะประกาศ List ของ OrderDetailItem ไว้ และเพิ่ม Override Method ที่ชื่อว่า getItemViewType(position: Int) เพื่อทำให้ RecyclerView แสดง ViewHolder แบบ Multiple Type ได้

จะเห็นว่า orderDetailItems ประกาศเป็นตัวแปรไว้ข้างในแทนที่จะอยู่ใน Constructor  เพราะในการใช้งานจริง Adapter จะถูกสร้างขึ้นมาก่อนที่จะมีข้อมูลอยู่แล้ว (เพราะข้อมูลต้องใช้เวลาในการโหลด) จึงทำให้เป็น var เพื่อให้กำหนดค่าเข้ามาในภายหลังดีกว่า

ที่ Constructor จะส่ง Function ที่ชื่อ onPositiveButtonClicked กับ onNegativeButtonClicked เข้ามาสำหรับ ViewHolder ที่มีปุ่มให้กด เพื่อทำให้ Click Event ส่งออกมาจาก ViewHolder ไปหา Activity ได้ (Adapter จะเป็นแค่ตัวกลางในการส่ง Event ออกไป)

ส่วน getItemViewType(...) ก็จะดึง type ที่อยู่ใน OrderDetailItem ที่เป็นค่า Integer ของ OrderDetailType ที่ได้กำหนดไว้ใน Data Class และ Object แต่ละตัวในบทความตอนที่แล้วนั่นเอง

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

สร้าง ViewHolder ประเภทต่างๆใน onCreateViewHolder ให้ครบ

ใน onCreateViewHolder มีไว้ให้นักพัฒนาสามารถสร้าง ViewHolder หลายๆแบบได้ตามใจชอบ โดยอิงจากค่า viewType ที่ส่งเข้ามาให้ ซึ่งเป็นค่าที่ได้จาก getItemViewType(...) นั่นเอง

ดังนั้นเจ้าของบล็อกจึงสามารถสร้าง ViewHolder แบบนี้ได้เลย

// OrderDetailAdapter.kt
class OrderDetailAdapter(/* .. */) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder = when (viewType) {
        OrderDetailType.TYPE_USER_DETAIL ->
            UserDetailViewHolder(ViewUserDetailBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        OrderDetailType.TYPE_TITLE ->
            TitleViewHolder(ViewTitleBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        OrderDetailType.TYPE_SECTION ->
            SectionViewHolder(ViewSectionBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        OrderDetailType.TYPE_ORDER ->
            OrderViewHolder(ViewOrderBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        OrderDetailType.TYPE_SUMMARY ->
            SummaryViewHolder(ViewSummaryBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        OrderDetailType.TYPE_TOTAL ->
            TotalViewHolder(ViewTotalBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        OrderDetailType.TYPE_NOTICE ->
            NoticeViewHolder(ViewNoticeBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        OrderDetailType.TYPE_BUTTON ->
            ButtonViewHolder(ViewButtonBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        OrderDetailType.TYPE_EMPTY ->
            EmptyViewHolder(ViewEmptyBinding.inflate(LayoutInflater.from(parent.context), parent, false))
        else ->
            throw NullPointerException("View Type $viewType doesn't match with any existing order detail type")
    }
    /* ... */
}

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

มาต่อกันที่ Method ตัวสำคัญอีกตัวที่ชื่อว่า onBindViewHolder

ใน onBindViewHolder จะให้เอาข้อมูลของ OrderDetailItem มากำหนดลงใน ViewHolder แต่ละตัว เพื่อแสดงผลตามต้องการ

// OrderDetailAdapter.kt
class OrderDetailAdapter(
    private val onPositiveButtonClicked: () -> Unit,
    private val onNegativeButtonClicked: () -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
        orderDetailItems.getOrNull(position)?.let { item ->
            when {
                holder is UserDetailViewHolder && item is OrderDetailItem.UserDetail -> holder.bind(item)
                holder is TitleViewHolder && item is OrderDetailItem.Title -> holder.bind(item)
                holder is SectionViewHolder && item is OrderDetailItem.Section -> holder.bind(item)
                holder is OrderViewHolder && item is OrderDetailItem.Order -> holder.bind(item)
                holder is SummaryViewHolder && item is OrderDetailItem.Summary -> holder.bind(item)
                holder is TotalViewHolder && item is OrderDetailItem.Total -> holder.bind(item)
                holder is ButtonViewHolder && item is OrderDetailItem.Button -> holder.bind({ onPositiveButtonClicked.invoke() }, { onNegativeButtonClicked.invoke() })
                holder is NoticeViewHolder && item is OrderDetailItem.Notice -> { /* Do nothing */
                }
                holder is EmptyViewHolder && item is OrderDetailItem.Empty -> { /* Do nothing */
                }
            }
        }
    }
    /* ... */
}

จะเห็นว่าเจ้าของบล็อกเช็คว่า holder เป็น ViewHolder ที่ต้องการหรือป่าว และ item ที่ดึงมาจาก orderDetailItems เป็นตัวที่ตรงกับ ViewHolder ตัวนั้นๆหรือไม่ ถ้าใช่ก็จะโยนข้อมูลเข้าไปใน ViewHolder ผ่านคำสั่ง bind(...) ที่ได้เตรียมไว้ในตอนที่แล้ว

การใช้ is  จะทำให้ Smart Cast แปลง holder กับ item ให้เป็นคลาสตามที่เขียนเช็คไว้โดยอัตโนมัติ ทำให้คำสั่ง bind(...) เป็นของ ViewHolder แต่ละตัวแยกกัน

และจะเห็นว่า onPositiveButtonClicked กับ onNegativeButtonClicked จะถูกเรียกด้วยคำสั่ง invoke() ตอนที่ปุ่มใน ButtonViewHolder ถูกกดแล้วส่ง Click Event ออกมาผ่านคำสั่ง bind(...) นั่นเอง

ตอนนี้ OrderDetailAdapter ของเจ้าของบล็อกก็พร้อมใช้งานแล้ว

จากตัวอย่างก่อนหน้า ใน updateOrderDetailItems(...) จะเห็นว่าเจ้าของบล็อกได้ใส่ Comment ให้กับสองคำสั่งนี้

// MainActivity.kt

// adapter.orderDetailItems = newItems
// adapter.notifyDataSetChanged()

ตอนนี้ก็เอา Comment ออกได้แล้ว เพราะว่าพร้อมใช้งานแล้วนั่นเอง

// MainActivity.kt
private fun updateOrderDetailItems(newItems: List<OrderDetailItem>) {
    adapter.orderDetailItems = newItems
    adapter.notifyDataSetChanged()
}

และเนื่องจาก Adapter มีการเพิ่ม onPositiveButtonClicked กับ onNegativeButtonClicked เข้าไปใน Constructor ด้วย ดังนั้นที่ Activity ก็จะต้องเตรียมให้เรียบร้อยด้วยเช่นกัน

class MainActivity : AppCompatActivity() {
    /* ... */
    private fun setupView() {
        adapter = OrderDetailAdapter(onPositiveButtonClicked, onNegativeButtonClicked, onOrderRemoveClicked)
        /* ... */
    }
    
    private val onPositiveButtonClicked: () -> Unit = {
        Toast.makeText(this, "Positive button clicked", Toast.LENGTH_SHORT).show()
    }

    private val onNegativeButtonClicked: () -> Unit = {
        Toast.makeText(this, "Negative button clicked", Toast.LENGTH_SHORT).show()
    }
}

เจ้าของบล็อกจะแสดงเป็น Toast แบบง่ายๆเพื่อให้เห็นว่า Click Event จาก ViewHolder สามารถส่งออกมาให้ Activity ได้จริง

และสำหรับคลาส FakeNetwork ที่พูดถึงในบทความตอนที่ 1 เจ้าของบล็อกเขียนจำลองขึ้นมาแบบนี้ครับ

// FakeNetwork.kt
object FakeNetwork {
    fun getFakeOrderDetail(callback: (orderDetail: OrderDetail) -> Unit) {
        Handler(Looper.getMainLooper()).postDelayed({
            callback.invoke(createFakeOrderDetail())
        }, 2000)
    }

    private fun createFakeOrderDetail(): OrderDetail {
        val fakeJson = "{\"food_list\":[{\"order_name\":\"Chicken\",\"amount\":2,\"price\":400},{\"order_name\":\"Egg\",\"amount\":24,\"price\":120}],\"book_list\":[{\"ISBN\":\"9780804139038\",\"book_name\":\"The Martian: A Novel\",\"author\":\"Andy Weir\",\"publish_date\":\"11 February 2014\",\"publication\":\"Broadway Books\",\"price\":314,\"pages\":384},{\"ISBN\":\"9781449327972\",\"book_name\":\"Embedded Android: Porting, Extending, and Customizing\",\"author\":\"Karim Yaghmour\",\"publish_date\":\"12 March 2013\",\"publication\":\"O'Reilly Media, Inc.\",\"price\":475,\"pages\":412},{\"ISBN\":\"9780545229937\",\"book_name\":\"The Hunger Games\",\"author\":\"Suzanne Collins\",\"publish_date\":\"1 September 2009\",\"publication\":\"Scholastic Inc.\",\"price\":279,\"pages\":384}],\"music_list\":[{\"artist\":\"Green Day\",\"album\":\"American Idiot\",\"release_date\":\"8 September 2004\",\"track\":9,\"price\":330}]}"
        return Gson().fromJson(fakeJson, OrderDetail::class.java)
}

เป็นการสร้าง JSON String ขึ้นมาแล้วแปลงด้วย GSON ให้กลายเป็นคลาส OrderDetail นั่นเอง

สำหรับผู้ที่หลงเข้ามาอ่านที่อ่านมาถึงจุดนี้แล้วอยากจะลองทดสอบโค้ด สามารถดาวน์โหลดตัวอย่างได้จากที่นี่ Lovely Recycler View — Part 2 Ending [GitHub]

akexorcist/LovelyRecyclerView
[Android] RecyclerView with complex layout and data - akexorcist/LovelyRecyclerView

ผลลัพธ์ที่ได้

จะเห็นว่า Recycler View สามารถทำงานได้ตรงที่ต้องการแล้ว

เย้ เย้ เย้ เสร็จแล้วๆ
.
.
.
เอ๊ะ! เดี๋ยวก่อนนะ เหมือนเห็นอะไรบางอย่างไม่ถูกต้อง
.
.

เฮ้ย!! ลืมไปเลยว่า Section ของแต่ละอันมันมีสีไม่เหมือนกันนนนนน

ก็นั่นล่ะฮะ เพราะงั้นเจ้าของบล็อกต้องแก้ไขให้ตรงกับที่ดีไซน์ไว้ในตอนแรก ซึ่งในบทความตอนต่อไปก็จะมาดูกันครับว่าวิธีสร้าง Recycler View แบบนี้ เวลามีการแก้ไขหรือเพิ่มอะไรเข้าไปมันจะเป็นยังไงบ้าง ยุ่งยากหรือป่าว หรือว่ามันจะง่ายขึ้นล่ะ? รอติดตามอ่านกันต่อในตอนที่ 3 นะ