จัดการ Android RecyclerView ให้อยู่หมัดด้วย Epoxy จาก Airbnb
หากพูดถึง RecyclerView แล้ว Android Developer ก็คงจะนึกถึง UI Component ที่แสดงผลแบบ List ที่มีคุณสมบัติในการ Reuse View เพื่อใช้ Memory อย่างมีประสิทธิภาพ
ซึ่งถ้า List Item ที่ต้องการแสดงผลตามีหน้าเหมือนกันหมด การใช้งาน RecyclerView ก็จะง่ายๆไม่ซับซ้อน แต่ในบางครั้งชีวิตจริงก็มักจะไม่ได้โรยด้วยกลีบกุหลาบ เมื่อต้องเจอกับโจทย์ที่มีความซับซ้อนยิ่งขึ้น UI มีหลากหลายมากขึ้น ตัวอย่างเช่น UI ต่อไปนี้
เห็นได้ว่า List Item มีหน้าตาต่างกัน ซึ่งการแสดงผล UI ลักษณะนี้ด้วย RecyclerView จะต้องทำการแยก View Type และ View Holder ของ List Item แต่ละแบบออกมา
จาก UI ด้านบน สามารถแบ่ง View Type ออกมาได้ถึง 5 แบบด้วยกัน ถ้าหาก Implement ด้วย RecyclerView Adapter โค้ดก็จะมีความซับซ้อนพอสมควร
(สำหรับท่านที่ยังไม่เคยทำมาก่อน แนะนำให้อ่านบทความที่เคยเขียนไว้ก่อนนะครับ)
และนี่เองเป็นที่มาของ Epoxy จาก Airbnb ที่จะมาช่วยให้การสร้าง UI ที่ซับซ้อนบน RecyclerView นั้นสะดวกยิ่งขึ้น ซึ่งบางคนอาจจะสงสัยว่ามันช่วยแค่นี้เองหรอ? เขียน if else ใน RecyclerView Adapter นิดหน่อยก็ได้มั้ง ไม่เห็นต้องจะมาใช้ Library อะไรให้วุ่นวาย เดี๋ยววันนี้ผมจะพาไปหาคำตอบกันครับ :)
Let’s Start
ในการใช้งาน Epoxy จะประกอบไปด้วยส่วนสำคัญ คือ
- EpoxyModel จะเป็นตัวแทนของ List Item แต่ละตัว ทำหน้าที่จัดการ State และ Logic ในการแสดงผลคล้ายกับ RecyclerView ViewHolder
- EpoxyController ทำหน้าที่จัดการกับ EpoxyModel คล้าย RecyclerView Adapter
EpoxyModel
การสร้าง EpoxyModel จะมีทั้งหมด 3 วิธีด้วยกัน ได้แก่ Custom Views, DataBinding และ ViewHolders
EpoxyModel by Custom Views
จากรูปด้านบนจะเห็นได้ว่ามี UI ที่เป็น ImageView อยู่หลายส่วน แต่มีการแสดงผลแตกต่างกัน ทั้งที่เป็นรูปอย่างเดียว, Horizontal List, Horizontal Grid List แต่ทั้งหมดจะมีส่วนประกอบที่เล็กที่สุดก็คือ ImageView
เราก็จะมาสร้างส่วนนี้กันก่อน ขอตั้งชื่อว่า BannerEpoxy
โดยใช้วิธีการสร้างแบบ Custom Views ซึ่งวิธีการก็คือสร้าง Custom Views แล้ว extends View Group ที่ต้องการและเพิ่ม Annotations @ModelView
ไว้ด้านบนเพื่อให้ Epoxy สามารถนำไป Generate เป็น EpoxyModel ได้
ณ ที่นี้ต้องการให้ ImageView ของเรามี Ratio เป็น 4:3
เลยทำการ extends ConstraintLayout และให้ ImageView อยู่ภายใน
โดย @ModelView
สามารถกำหนด Custom Layout ด้วย defaultLayout
เพื่อให้ Inflate Layout XML ที่ต้องการ
สิ่งที่ต่างจาก Custom Views คือ Root View ใน Layout XML จะต้องเป็น Epoxy Model ที่สร้างขึ้นมา เหมือนกับการใช้ Tag <merge>
คือจะเอา Child Layout ที่อยู่ภายใต้ Inflate เข้าไปใน View Group นั่นเอง
จากที่ได้เกริ่นไว้ด้านบนว่า EpoxyModel ทำหน้าที่ในการจัดการ State และ Logic ในการแสดงผลของแต่ละ List Item โดยจะเรียก State นี้ว่า Properties
ซึ่งสามารถกำหนด Properties ให้กับ EpoxyModel ด้วย Annotation
@ModelProp
สำหรับ Object@TextProp
สำหรับ String Resource@CallbackProp
สำหรับ Callback หรือ Listener เช่น OnClickListener
หลังจาก Build Project แล้ว Epoxy จะทำการ Generate Epoxy Model ขึ้นมา โดยวิธี Custom Views จะ Generate Class ใหม่ด้วย ชื่อ Class เดิมตามด้วย Model_
จะได้เป็น BannerEpoxyModel_
และจะ Generate Getter/Setter
method สำหรับ Properties แต่ละตัวด้วย ซึ่งจะนำไปใช้ร่วมกับ Epoxy Controller ในภายหลัง
ต่อมาสร้าง LabelEpoxy
ซึ่งไม่ต้องการ Custom Layout ใดๆ เป็นเพียง TextView ธรรมดา ก็สามารถทำได้ด้วย Annotation @ModelView
เหมือนเดิมและระบุขนาดลงไปผ่าน autoLayout
EpoxyModel by ViewHolder
นอกจาก Custom Views แล้วยังสามารถสร้างด้วยวิธี ViewHolder ซึ่งวิธีนี้จะเหมือนกับการสร้าง RecyclerView ViewHolder ปกติ โดยต้อง extends EpoxyModelWithHolder
และใส่ Annotation @EpoxyModelClass
พร้อมระบุ Custom Layout ที่ต้องการด้วย layout
สุดท้ายสร้าง ViewHolder แต่ด้วยความที่ Kotlin Extension ยังไม่รองรับการ Cache findViewById
บน ViewHolder เลยทำให้ต้องมี Helper Class มาช่วย ดูโค้ดได้ที่ KotlinEpoxyHolder
วิธีนี้จะใช้ @EpoxyAttribute
แทน @ModelProp
และมี method bind()
สำหรับจัดการกับ State และ Logic ของ View ซึ่งจะถูกเรียกทุกครั้งที่ View กำลังจะแสดงผลเหมือนกับ method onBindViewHolder()
ของ RecyclerView ส่วน Model ที่ Generate มาจะตามด้วย _
เท่านั้นก็จะได้เป็น ProductItemEpoxy_
EpoxyController
สร้างโดยการ extends EpoxyController
ซึ่งจะมีลักษณะการทำงานแบบ One direction Data flows คือ เมื่อมีข้อมูลเข้ามา ข้อมูลจะถูกส่งไปในทิศทางเดียวตั้งแต่ EpoxyController ไปจนถึงแสดงผลที่ View และเมื่อมีการเปลี่ยนแปลงก็จะมาเริ่มต้นใหม่อีกครั้งตาม Life Cycle
การ Trigger ให้ EpoxyController เริ่ม Life Cycle ทำได้ด้วยการเรียก Controller.requestModelBuild()
และส่งผลให้ buildModels()
ของ EpoxyController ถูกเรียก ซึ่ง method นี้เองที่เป็นส่วนที่ต้อง Implement เพื่อสร้าง Instance ของ EpoxyModel และให้ EpoxyController นำไปแสดงผลบน RecyclerView
ลองมาดูกันว่าถ้าอยากจะแสดง Label ตามด้วย Banner จะทำยังไง
จากโค้ดจะพบว่า EpoxyController รองรับ Instance ของ EpoxyModel ที่ Generate เท่านั้นและสิ่งที่ขาดไม่ได้เลย คือ ต้องกำหนด uniqueid
ของ EpoxyModel
ส่วนคำสั่ง .addTo()
คือการ Add EpoxyModel เข้า EpoxyController เพื่อนำไปแสดงผลใน RecyclerView ส่วน .addIf()
จะต่างออกไปคือจะแสดงผลก็ต่อเมื่อเงื่อนไขเป็นจริงเท่านั้น และลำดับการแสดงผลบน RecyclerView นั้นจะขึ้นกับลำดับในการ Add ด้วยเช่นกัน
Integration
เมื่อมีทั้ง EpoxyController และ EpoxyModel แล้ว อีกสิ่งที่ต้องการก็ คือ RecyclerView เพื่อแสดงผล ซึ่งการให้ Epoxy ทำงานร่วมกับ RecyclerView นั้นสามารถเรียก Controller.getAdapter()
เพื่อให้ RecyclerView.setAdapter()
ได้เลย
หรือจะใช้ EpoxyRecyclerView
แทน RecyclerView ซึ่งมาพร้อมกับ EpoxyRecyclerView.setController()
และ Built-in functions ต่างๆที่จะทำให้ทำงานร่วมกันสะดวกยิ่งขึ้น
เมื่อมีครบตามด้านบนแล้วก็สามารถใช้งาน Epoxy ได้แล้ว เย้!!
Add Ons
นอกจากวิธีการใช้งานเบื้องต้นที่ได้กล่าวไป สิ่งที่ทำให้ Epoxy น่าใช้งานมากยิ่งขึ้นก็คือเหล่า Add Ons ที่เค้าเตรียมให้ ลองมาดูตัวที่น่าจะได้ใช้บ่อย
Carousels (Horizontal List)
Carousel
เป็น Epoxy Add Ons ที่มาช่วยให้การทำ Horizontal List นั้นสะดวกมาก
จากรูปด้านบนจะเห็นว่า UI ที่ต้องการมี Horizontal List อยู่หลายจุด ซึ่งถ้าใช้ RecyclerView Adapter เราจะต้องทำการสร้าง ViewHolder ที่มี RecyclerView ซ้อนอยู่ด้านในอีกที ซึ่ง Carousel
นี่เองที่จะมาช่วยให้เราไม่ต้องทำส่วนนี้เอง
จากโค้ดต้องการให้แสดง Horizontal List ของ ProductItemEpoxy
ที่เราสร้างไว้ด้านบน พระเอกของงานนี้ คือ CarouselModel_
เพียงทำการสร้าง List ของ ProductItemEpoxy ส่งเข้าไปใน models()
แล้ว Epoxy ก็จะจัดการให้ ง่ายสุดๆไปเลย
ไม่หมดเพียงเท่านี้ ยังมีอีก อาทิ Swipe and Drag Support, Grid Support, Paging Support, Prefetching Images, Cyclic Adapters และอีกมากมาย สามารถดูเพิ่มเติมได้ที่ Epoxy Wiki
จะเห็นได้ว่า Epoxy มีความคล้ายกับ RecyclerView Adapter แต่จะมีการแยกส่วนชัดเจน ทำให้จัดการโค้ดได้ดีขึ้น เหมาะสำหรับการทำ UI ที่ซับซ้อน แต่ทั้งนี้ก็ต้องแลกมาด้วยการต้องไปเรียนรู้วิธีการใช้งานและข้อจำกัดต่างๆที่นอกเหนือจาก RecyclerView ปกติ เพราะฉะนั้นก่อนที่จะนำเอาไปใช้แนะนำศึกษา Documents หรือลองเล่นดูก่อนและที่สำคัญอย่าลืมปรึกษาทีมด้วยนะครับ
ด้วยรักและหวังดี สวัสดีครับ (≧▽≦)
ปล. โค้ดเต็มๆสามารถดูได้ที่ Github Repository