จัดการ Android RecyclerView ให้อยู่หมัดด้วย Epoxy จาก Airbnb

Teeranai.P
King Power Click
Published in
4 min readJul 19, 2019

หากพูดถึง RecyclerView แล้ว Android Developer ก็คงจะนึกถึง UI Component ที่แสดงผลแบบ List ที่มีคุณสมบัติในการ Reuse View เพื่อใช้ Memory อย่างมีประสิทธิภาพ

ซึ่งถ้า List Item ที่ต้องการแสดงผลตามีหน้าเหมือนกันหมด การใช้งาน RecyclerView ก็จะง่ายๆไม่ซับซ้อน แต่ในบางครั้งชีวิตจริงก็มักจะไม่ได้โรยด้วยกลีบกุหลาบ เมื่อต้องเจอกับโจทย์ที่มีความซับซ้อนยิ่งขึ้น UI มีหลากหลายมากขึ้น ตัวอย่างเช่น UI ต่อไปนี้

ของลดราคาเยอะเว่อ!!!

เห็นได้ว่า List Item มีหน้าตาต่างกัน ซึ่งการแสดงผล UI ลักษณะนี้ด้วย RecyclerView จะต้องทำการแยก View Type และ View Holder ของ List Item แต่ละแบบออกมา

RecyclerView with multiple view types by Epoxy
RecyclerView with multiple view types

จาก 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

BannerEpoxy

จากรูปด้านบนจะเห็นได้ว่ามี 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

LabelEpoxy

EpoxyModel by ViewHolder

นอกจาก Custom Views แล้วยังสามารถสร้างด้วยวิธี ViewHolder ซึ่งวิธีนี้จะเหมือนกับการสร้าง RecyclerView ViewHolder ปกติ โดยต้อง extends EpoxyModelWithHolder และใส่ Annotation @EpoxyModelClass พร้อมระบุ Custom Layout ที่ต้องการด้วย layout

ProductItemEpoxy

สุดท้ายสร้าง 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

Epoxy Controller One direction Data flows

การ 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 นั้นสะดวกมาก

Carousel / 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

--

--