[Dev] รู้จักกับ ViewHolder Pattern สำหรับ ListView, GridView และการมาของ RecyclerView

ViewHolder Pattern เป็นรูปแบบของการจัดการ View ภายใน Adapter ที่เรากำหนดให้กับ ListView หรือ GridView เพื่อให้เกิดประสิทธิภาพสูงสุด ซึ่งเป็น Best Practice หนึ่ง ที่ Google แนะนำ ซึ่งจะส่งผลให้การ Scroll ไม่เกิดอาการกระตุก

Android-Developer-logo_ViewHolder-Pattern

สาเหตุหลักที่ทำให้เกิด ViewHolder Pattern เพื่อปรับปรุงคุณภาพนั้น ก็เนื่องมาจากการทำการ Inflate Layout และ findViewById() ซ้ำ ๆ บ่อย ๆ เกินความจำเป็น ViewHolder Pattern จึงออกแบบมาช่วยให้ไม่เกิดการทำแบบเดิมซ้ำ ๆ นั่นเอง

สำหรับบทความตอนนี้ จะเน้นไปที่การทำ ViewHolder ภายใน Custom Adapter เท่านั้น สำหรับผู้ที่ยังไม่รู้จักการทำ Custom Adapter ลองไปอ่านบทความตอน Custom List View เบื้องต้น จาก Blog เพื่อนบ้านก่อนนะครับ

 

List of contents

 

ก่อนอื่น ผมขอยกตัวอย่าง Layout สำหรับเป็น View Item ง่าย ๆ โดยมี TextView และ CheckBox อย่างละตัว ดังนี้

Layout สำหรับ View Item
Layout สำหรับ View Item

และออกแบบ Class สำหรับเก็บข้อมูลไว้แสดงบน TextView กับสถานะการ Check ของ CheckBox ดังนี้

และเตรียม Function ในรูปแบบ Static Method ไว้ใน Class SimulateItems ไว้ติ๊ต่างว่าเป็นข้อมูลจากภายนอก ที่จะนำไปแสดงผลในหัวข้อต่อ ๆ ไป ดังนี้

 

เอาละ พร้อมแล้ว… ไปดูการทำ ViewHolder สำหรับ ListView กันก่อนเลย…

 

ViewHolder for ListView

สำหรับ ListView นั้น ขอใช้ Layout ตามนี้แล้วกันนะ

Layout สำหรับ ListView
Layout สำหรับ ListView

ในส่วนของ Activity โดยปกติเราก็ทำ Custom Adapter ขึ้นมา แล้ว setAdapter() ให้กับ ListView แบบ Code ต่อไปนี้ Custom Adapter ก็คือ Class ListViewAdapter โดยจะโยนข้อมูลเข้าไปให้ทาง Constructor ด้วยเลย

จริง ๆ แค่นี้ ListView ก็ผูกกับ View Item และข้อมูล ซึ่งก็พอทำงานได้แล้วล่ะ แต่มันยังไม่ใช่ Best Practice ไง เห็นบรรทัดที่ 43 และ 45-46 ไม๊ล่ะครับ ทุกครั้งที่ Method getView() ถูกเรียก มันจะไป Inflate Layout มาจาก XML File และทำการ findViewById() ใหม่ทุกครั้ง ที่เป็นสาเหตุหลักของอาการกระตุก เมื่อ Scroll นั่นเอง

ทบทวนกันนิด Method getView() ของ Adapter จะถูกเรียกทุกครั้งที่ View Item กำลังจะถูกเรียกขึ้นมาแสดงบนจอ ส่วน Item ไหนที่ล้นไปอยู่นอกจอ จะยังไม่ถูกเรียกเข้ามา

และถ้าสังเกตนิดนึง จะเห็นว่า Android Studio ได้บ่นเราออกมาด้วยนะ ว่าให้ไปใช้ ViewHolder Pattern ซะ!?

Android Studio แนะนำการทำ ViewHolder Pattern
Android Studio แนะนำการทำ ViewHolder Pattern

ทีนี้ เรามาจัดการเรื่อง Inflate Layout กันก่อน โดยการเช็คก่อนว่าเคย Inflate มารึยัง ดังนี้

ก็เพราะเมื่อเรา Inflate Layout มาแล้ว เรามีการเก็บไว้ใน View และ Return คืนออกไปจาก Method getView() และมันจะถูกคืนกลับมาใหม่ เมื่อมีการ Scroll มายังตำแหน่งเดิมที่เคยเก็บ View ไปแล้ว ผ่านทาง Parameter ตัวที่ 2 ของ Method getView() นั่นเอง ในครั้งแรกจึงยังเป็น null อยู่ ก็เพิ่มเงื่อนไขเช็ค null ซะ แค่นี้ก็ไม่ต้อง Inflate ซ้ำแล้ว

ได้เวลาของ ViewHolder Pattern ละ

ขั้นแรกก็ สร้าง Class ViewHolder ไว้เป็น Inner Class ของ Adapter ก่อนเลย และให้ Constructor มี Parameter สำหรับรับ View ที่ Inflate มา แล้วทำการ findViewById() ไปเก็บไว้ในตัวแปร ดังนี้

ที่ Method getView() ทำการปรับปรุง Code โดยเมื่อทำการ Inflate Layout มาแล้ว ก็ทำการสร้าง Instance จาก Class ViewHolder พร้อมโยน View ไปให้มัน ต่อด้วยการฝาก ViewHolder ไว้กับ Method setTag() ของ View

แต่ถ้าเคย Inflate Layout มาแล้ว ก็ใช้ Method getTag() ของ View ดึง ViewHolder คืนกลับมา ดังใน Code ด้านล่าง

Method setTag() และ getTag() จะใช้ฝากอะไรก็ได้ เพราะมันเก็บเป็นชนิด Object เมื่อสั่ง getTag() จึงจำเป็นต้อง Casting ให้ถูกต้องด้วย

ใครถนัด Visual Studio คงคุ้นตากับ Property Tag ที่เก็บอะไรก็ได้ Concept เดียวกันเลย

ยังไม่จบ… ยังจำกันได้ไม๊ครับว่า เรามี CheckBox อยู่ ซึ่งสามารถจิ้มเพื่อเปลี่ยนค่าได้ แต่เรายังไม่ได้จัดการอะไรกับมันเลย เพราะฉะนั้น จะยังมี Bug อยู่นิดนึง ถ้า CheckBox เปลี่ยนสถานะ Checked/Unchecked แล้ว Scroll ออกไป เมื่อกลับเข้ามาใหม่ มันจะไม่ได้เก็บไว้ เพราะใน Method getView() เราเอาข้อมูลที่ส่งมาจากนอก Adapter ไปแสดงผล จึงจำเป็นต้องทำ Listener ให้กับ CheckBox เพื่อเก็บค่าสถานะคืนกลับไปด้วย ดังนี้

ซึ่งค่าสถานะ Check/Unchecked ตรงกับข้อมูลของ Class LoremItem ที่ออกแบบไว้ตั้งแต่แรกแล้ว จึงเก็บค่าคืนกลับไปได้เลย

อย่าลืมกำหนด final ตรงบรรทัดที่ 2 ให้กับ Parameter ตัวแรกด้วยนะ เพราะมันคือ Position ของ List Item เนื่องจากบรรทัดที่ 16 จำเป็นต้องใช้ Position ไประบุตำแหน่ง ซึ่งมันอยู่ใน Method ของ Anonymous Class ไม่สามารถใช้ตัวแปรข้างนอกตรง ๆ ได้

เราก็จะได้ ListView ที่มีการใช้งาน ViewHolder รวมถึงมีการเก็บข้อมูลจากการเปลี่ยนแปลงของ List Item ไว้ได้ด้วยแล้วละ

 

ViewHolder for GridView

มาดู GridView กันบ้าง การใช้งาน GridView นั้น เหมือนกับ ListView ทุกประการ ต่างกันที่ View ที่แสดงผลแค่นั้นเอง

เริ่มจาก Layout ก่อน…

Layout สำหรับ GridView
Layout สำหรับ GridView

ส่วนของ Activity ก็เหมือน ListView เลย ไม่อธิบายซ้ำละกันเน๊อะ

แล้วก็สร้าง Class ViewHolder เหมือนเดิมเป๊ะ ๆ เด๊ะ ๆ

แล้วนำ ViewHolder ไปใช้งานใน Method getView()

เพื่อให้แตกต่างกับ ListView คราวนี้ผมจะให้ TextView ของแต่ละ Item นั้น สามารถจิ้มแช่ แล้วมี Dialog ขึ้นมาเป็นตัวเลือกสีที่จะกำหนดไปเป็น Background Color ของ TextView ละกัน ดังนี้

ถ้ายังจำกันได้ในหัวข้อ ListView ตัว CheckBox มีการเปลี่ยนแปลง เราจึงต้องเก็บค่าไว้ แต่… ค่าสีที่กำหนดให้ Background ของ TextView นี่ จะเก็บยังไง ในเมื่อ Class LoremItem ไม่ได้ออกแบบให้เก็บค่าไว้

คิดกันออกไม๊เอ่ย…

ก็เพิ่มใน Class LoremItem ซะซิ ง่ายจะตาย ก็จะได้เป็นดังนี้

ซึ่งผมออกแบบให้เก็บค่า Index ของสีที่ตรงกับตำแหน่งที่เลือกบน Dialog เพราะฉะนั้น ก็ทำการเก็บค่าตอนเลือกสีแล้ว ดังนี้

สุดท้าย อย่าลืมคืนค่าที่เก็บไปแล้ว ใน Method getView() ด้วย

เสร็จแล้วครับ GridView ที่ใช้ ViewHolder และเก็บการเปลี่ยนแปลงของ Item ได้แล้ว

กรณีที่ไม่อยากให้ไปยุ่งอะไรกับ Class ที่ออกแบบมาเพื่อเก็บข้อมูลอย่างเดียว เพราะอาจจะใช้ในเรื่องอื่นอีก สามารถเพิ่มตัวแปรใน Adapter มาเก็บแทนก็ได้ ดังนี้

แค่นี้ก็สามารถเก็บค่าไว้ใน Adapter ได้แล้ว

เห็นตามกลุ่ม Android Developer มีคนถามเกี่ยวกับการเก็บค่าจากการเปลี่ยนแปลงของ View Item อยู่บ่อย ๆ เพราะไม่รู้ต้องทำอย่างไร ถ้าอ่านมาถึงตรงนี้ คงรู้แล้วละเน๊อะ

 

RecyclerView.ViewHolder

RecyclerView นั้น ออกแบบมาเพื่อแก้ปัญหาของการทำ ViewHolder Pattern ด้วยการบังคับใช้ ViewHolder ซะเลย ซึ่ง RecyclerView นั้น เป็น View ที่อยู่ใน Support Library v7 ซึ่งจะต้องกำหนด Dependencies ให้กับ Project ของเรา ในไฟล์ build.gradle ก่อน ดังนี้

เริ่มกันที่ Layout กันก่อนเลยละกัน

Layout สำหรับ RecyclerView
Layout สำหรับ RecyclerView

สำหรับการ Preview Layout สำหรับ RecyclerView บน Android Studio 1.3 ยังไม่สามารถ Preview ด้วย View Item ที่เตรียมไว้ได้

ต่อด้วยส่วนของ Activity กันเลย

คล้าย ๆ กับ ListView ใช่ไม๊ครับ

ตรงที่ Comment ไว้นั้น คือส่วนที่เราจะสร้าง Custom Adapter ซึ่งที่ผ่านมา สำหรับ ListView และ GridView เราจะ Inherit (extends) มาจาก class BaseAdapter แต่สำหรับ RecyclerView นั้น ถูกบังคับให้ใช้ RecyclerView.Adapter แทน ซึ่ง RecyclerView.Adapter จะบังคับใช้ RecyclerView.ViewHolder อีกทอดนึง

เพราะฉะนั้น เราจะมาเริ่มจากการสร้าง RecyclerView.ViewHolder กันก่อน ดังนี้

คล้าย ๆ กับที่เราสร้าง class ViewHolder เองเลย เพียงแต่คราวนี้เราต้อง extends RecyclerView.ViewHolder มา และสร้างไว้เป็น Inner Class ของ Custom Adapter เช่นกัน

ViewHolder เสร็จละ… คราวนี้จะใช้งานมันบ้าง โดยไปทำ Custom Adapter ให้เรียบร้อย ซึ่งจะต้อง extends RecyclerView.Adapter ก่อน

จะเห็นว่า RecyclerView.Adapter<VH> นั้น จะต้องระบุ VH ด้วย ซึ่งมันก็คือ RecyclerView.ViewHolder เราก็แค่ระบุ class ที่เราสร้างไว้ลงไป ใน Code ด้านบนก็คือ RecyclerViewAdapter.ViewHolder นั่นเอง

ทำการ Implement Method ที่ RecyclerView.Adapter บังคับ ดังนี้

ก่อนอื่น ไปสร้าง Constructor กันก่อน ซึ่งเราจะนำข้อมูลเข้ามายัง Adapter ทาง Constructor เช่นเดียวกับ ListView และ GridView ดังนี้

เมื่อมีข้อมูลแล้ว ก็ Implement Method getItemCount() ได้แล้ว…

หลังจากนั้น มา Implement Method onCreateViewHolder() กันต่อเลย โดยการ Inflate Layout มาลงตัวแปร View แล้วสร้าง ViewHolder พร้อมกับส่ง View ไปทาง Constructor ต่อด้วย return ViewHolder ออกไปเลย

Method สุดท้าย onBindViewHolder() จะส่ง ViewHolder มาให้ พร้อมตำแหน่งของ View Item ทาง Parameter ก็จัดการผูกข้อมูลกับ View ซะให้เรียบร้อย

ถ้าแค่แสดงผลข้อมูล ก็เสร็จแล้วครับ แต่… ลืมอะไรรึเปล่า…

อย่าลืมเอา Comment บรรทัดนี้ออกนะ สร้าง Custom Adapter แทบตาย แต่ลืมเรียกใช้มัน

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

Code ด้านล่างนี้ จะเก็บการเปลี่ยนแปลงของ CheckBox เช่นเดียวกับในหัวข้อ ViewHolder for ListView เลย โดยเราจะเขียนคำสั่งเพิ่มเติมใน Method onBindViewHolder() เนื่องจากว่ามันเป็น Method เดียว ที่สามารถบอกตำแหน่งของ View Item ได้ ผ่านทาง Parameter

สำหรับวิธีใช้ RecyclerView อย่างละเอียด อ่านต่อได้ที่ RecyclerView สิ่งใหม่ที่กูเกิ้ลหวังว่าจะทำให้แอพฯแอนดรอยด์ดีขึ้น จาก Blog เพื่อนบ้านเลยครับ

 

หวังว่าผู้ที่ได้อ่านบทความนี้ จะเข้าใจ ViewHolder มากขึ้น และนำไปใช้งานได้อย่างถูกต้อง

และสำหรับ Source Code ทั้งหมด ผมได้เอาขึ้น GitHub ไว้ให้แล้วนะครับ

 

อ้างอิง – Making ListView Scrolling Smooth – Hold View Objects in a View Holder

Leave a Reply

Your email address will not be published. Required fields are marked *

 

This site uses Akismet to reduce spam. Learn how your comment data is processed.