28 ตุลาคม 2559

[Android Code] ว่าด้วยเรื่อง Recycler View กับการใช้งานจริงในแบบฉบับเจ้าของบล็อก ตอนที่ 1



        หลังจากที่ Recycler View ถูกเพิ่มเข้ามาใน Android 5.0 Lollipop และกลายเป็น Support Library ที่สามารถทำงานบนแอนดรอยด์เวอร์ชันต่ำกว่านั้นได้ จึงทำให้เจ้าของบล็อกได้ใช้ Recycler View แทน List View ไปโดยปริยาย เนื่องจากการใช้งานมันที่ถึงแม้จะเขียนเยอะแต่ก็ทำให้โค้ดออกมาเป็น Pattern ที่ถูกต้อง ไม่เหมือน List View ที่ยังดูค่อนข้างสะเปะสะปะ จะเขียนยังไงก็ตามใจฉัน

บทความที่เกี่ยวข้อง

        • ว่าด้วยเรื่อง Recycler View กับการใช้งานจริงในแบบฉบับเจ้าของบล็อก ตอนที่ 1
        • ว่าด้วยเรื่อง Recycler View กับการใช้งานจริงในแบบฉบับเจ้าของบล็อก ตอนที่ 2
        • ว่าด้วยเรื่อง Recycler View กับการใช้งานจริงในแบบฉบับเจ้าของบล็อก ตอนที่ 3
        • ว่าด้วยเรื่อง Recycler View กับการใช้งานจริงในแบบฉบับเจ้าของบล็อก ตอนที่ 4
        • ว่าด้วยเรื่อง Recycler View กับการใช้งานจริงในแบบฉบับเจ้าของบล็อก ตอนที่ 5
        • ว่าด้วยเรื่อง Recycler View กับการใช้งานจริงในแบบฉบับเจ้าของบล็อก ตอนที่ 6


        จะว่าไปเจ้าของบล็อกก็ไม่ค่อยได้เขียนบทความเกี่ยวกับ Recycler View ซักเท่าไรเนอะ บทความพื้นฐานเกี่ยวกับ Recycler View ก็ไม่ได้เขียน เพราะว่ามีนักพัฒนาหลายๆคนเขียนบทความเกี่ยวกับเรื่องนี้แล้ว ถ้าผู้ที่หลงเข้ามาอ่านยังไม่เคยอ่านก็แนะนำให้ไปอ่านเพื่อทำความเข้าใจใน Recycler View ก่อนนะครับ

        • [โหมด Geek] RecyclerView สิ่งใหม่ที่กูเกิ้ลหวังว่าจะทำให้แอพฯแอนดรอยด์ดีขึ้น
        • [ANDROID] มาลองทำ Multiple layout บน ListView/RecyclerView กัน
        • [Dev] รู้จักกับ ViewHolder Pattern สำหรับ ListView, GridView และการมาของ RecyclerView

แล้วบทความนี้จะพูดถึง Recycler View แบบไหนล่ะ?

        ก็อย่างที่บอกในตอนแรกว่าเจ้าของบล็อกเลิกใช้ List View และเปลี่ยนมาใช้ Recycler View ในการทำงานแล้ว เพราะตัวมันมีความยืดหยุ่นในการใช้งานที่สูงกว่า ในขณะเดียวกันก็กำหนด Pattern ในการเขียนมาให้เลย จึงมั่นใจได้นิดนึงว่ารูปแบบโค้ดจะไม่เละเทะ


        อะไรนะ? จะให้อธิบายวิธีการสร้าง Recycler View แบบภาพข้างบนนี้น่ะหรอ? ไม่มีทางแน่นอน เพราะนี่มัน Recycler View ธรรมดาเกินไป๊

        บ่อยครั้งที่เจ้าของบล็อกต้องเขียนแอปฯที่มี Layout โหดร้ายพอสมควร ยกตัวอย่างว่าต้องดึงข้อมูลจาก Web Service แล้วเอามาแสดงในรูปแบบประมาณนี้


        ฝั่งซ้ายมือคือผลลัพธ์ที่ต้องการ ส่วนฝั่งขวามือคือ JSON ที่สมมติขึ้นมาว่ารับมาจาก Web Service

        เมื่อพิจารณาจากข้อมูลจะเห็นว่ามีข้อมูลอยู่ 3 ส่วนด้วยกัน ซึ่งแต่ละส่วนก็จะมีข้อมูลแตกต่างกันออกไป และสุดท้ายก็ต้องทำออกมาให้เหมือนกับภาพตัวอย่างให้ได้อยู่ดี และเวลาเลื่อนขึ้นลงก็จะต้องเลื่อนพร้อมกันทั้งหมด ไม่ใช่เลื่อนแค่ส่วนที่เป็นข้อมูล Dynamic



        ถ้าสังเกตดีๆก็จะเห็นว่าในภาพตัวอย่าง นอกจากส่วนที่เป็นข้อมูล ก็ยังมีส่วนอื่นๆประกอบอยู่ด้วยมีทั้งชื่อผู้ใช้งาน, หัวข้อของข้อมูลแต่ละชุด, สรุปรวมรายการทั้งหมด และปุ่มยืนยัน/ยกเลิกรายการ ซึ่งบางส่วนก็เป็นส่วนที่มีการ Fixed ข้อมูลไว้ และบางส่วนก็มีการเอาข้อมูลมาคำนวณออกมาเพื่อแสดงในช่องนั้นๆ

        ผู้ที่หลงเข้ามาอ่านบางคนเห็นโจทย์แบบนี้ก็คงใช้วิธีจัด Layout ส่วนที่ Fixed ข้อมูลเตรียมไว้แล้วส่วนที่เป็นข้อมูลแบบ Dynamic ก็สร้าง Linear Layout ขึ้นมาเพื่อ Add View เพิ่มเข้าไปในทีหลัง โดยมี Scroll View ครอบอยู่ทั้งหน้าเพื่อให้เลื่อนขึ้นลงได้

        ซึ่งวิธีดังกล่าวเป็นวิธีที่ไม่ค่อยถูกต้องซักเท่าไร เนื่องจากการใช้ Scroll View แล้วยัด View ทั้งหมดไว้ข้างในนั้น จะเกิดปัญหาเรื่องการ View ที่ยังกินทรัพยากรอยู่ เพราะ View ที่เก็บไว้ใน Scroll View ทั้งหมด จะไม่มีการ Recycle เกิดขึ้น


        โดยปัญหาที่ว่าอาจจะไม่ค่อยเห็นผลถ้าข้อมูลมีจำนวนไม่เยอะมาก แต่ถ้าข้อมูลมีซัก 30 ชุดขึ้นไปก็คงไม่สนุกซักเท่าไร... ตอน Add View เข้าไปพร้อมๆกันก็ทำให้ค้างชั่วขณะ และเวลาเลื่อนดูข้อมูลไปมาก็ไม่ลื่น...

Recycler View พระเอกของงานนี้

        เพื่อแก้ปัญหาการใช้ Scroll View แล้วทำให้สิ้นเปลืองทรัพยากรให้กับ View ที่ยังไม่ได้แสดงโดยไม่จำเป็น จึงทำให้ Recycler View เข้ามาแก้ปัญหานี้โดยเฉพาะ (List View ก็ทำได้เหมือนกัน แต่เขียนยุ่งยากกว่า จึงไม่ค่อยแนะนำ)

        แล้วจะเอา Recycler View มาช่วยแก้ปัญหานี้ยังไงล่ะ?

        ก็ให้ทั้ง View ทุกตัวที่แสดงอยู่ในหน้านี้ เป็นส่วนหนึ่งของ Recycler View ไปเลยสิ!!


        เพื่อให้สามารถเลื่อนได้ทั้งหน้าจอและ View มีการ Recycle เมื่อเลื่อนออกนอกจอ ดังนั้นการใช้ Recycler View ในงานแบบนี้จึงตอบโจทย์ได้ดีกว่าการใช้ Scroll View

        แต่ความยากของการใช้ Recycler View อยู่ที่ว่าจะทำยังไงให้ Recycler View แสดงผลลัพธ์ออกมาตามที่ต้องการได้ต่างหากล่ะ

กำหนดชื่อเรียกแต่ละส่วนให้เรียบร้อยก่อน

        ก่อนจะเริ่มสร้าง Recycler View แล้วเขียนโค้ด ผู้ที่หลงเข้ามาอ่านจะคิดก่อนว่า View ที่จะใช้แสดงใน Recycler View ตัวนี้มีทั้งหมดกี่แบบ ซึ่งเจ้าของบล็อกแบ่งออกมาได้ทั้งหมด 9 แบบดังนี้


        จะเห็นว่ามีกระทั่ง Empty View ด้วย เพื่อกำหนดเป็นพื้นที่ว่างเพื่อให้เกิดระยะห่างระหว่าง View ตัวอื่นๆ

        เอาล่ะ มาเริ่มเตรียมโค้ดในแต่ละส่วนกันดีกว่า~

เปลี่ยน JSON ให้กลายเป็น Model Class (POJO)

        ขั้นตอนนี้คงไม่อธิบายว่าจะแปลงจาก JSON ยังไง เพราะส่วนใหญ่ก็คงจะใช้ GSON ในการแปลงกันอยู่แล้ว แต่จาก JSON ในตัวอย่าง ก็จะต้องสร้าง Model Class แบบนี้

OrderDetail.java
import com.google.gson.annotations.SerializedName;

import java.util.List;

public class OrderDetail {
    @SerializedName("food_list")
    private List<Food> foodList;

    @SerializedName("book_list")
    private List<Book> bookList;

    @SerializedName("music_list")
    private List<Music> musicList;

    ...

    public static class Food {
        @SerializedName("order_name")
        private String orderName;
        private int amount;
        private int price;

        ...
    }

    public static class Book {
        @SerializedName("ISBN")
        private String isbn;
        @SerializedName("book_name")
        private String bookName;
        private String author;
        @SerializedName("publish_date")
        private String publishDate;
        private String publication;
        private int price;
        private int pages;

        ...
    }

    public static class Music {
        private String artist;
        private String album;
        @SerializedName("release_date")
        private String releaseDate;
        private int track;
        private int price;

        ...
    }
}

        สำหรับ @SerializableName นั้นเป็น Annotation ของ GSON ที่ใช้ Map ข้อมูลให้ตรงกับชื่อที่ต้องการเปลี่ยน และในตัวอย่างนี้ก็จะมี Getter Setter อยู่ด้วย แต่เพื่อไม่ให้ยาวเกินไปจึงไม่ใส่ในนี้เพื่อให้ตัวอย่างดูสั้นกระชับมากขึ้น

        ดังนั้นข้อมูลที่ได้มาเป็น JSON จะถูกแปลงให้กลายเป็นคลาส OrderDetail เพื่อเอาไปใช้งานนั่นเอง

กำหนด Type สำหรับ View Holder

        ประกาศ Type ของ View ทั้ง 9 แบบให้ครบ โดยแต่ละ Type จะกำกับด้วยตัวเลขที่ต่างกันออกไป (ระวังอย่าให้เลขซ้ำกัน)

OrderDetailType.java
public class OrderDetailType {

    public static final int TYPE_EMPTY = 0;
    public static final int TYPE_USER_DETAIL = 1;
    public static final int TYPE_TITLE = 2;
    public static final int TYPE_SECTION = 3;
    public static final int TYPE_ORDER = 4;
    public static final int TYPE_SUMMARY = 5;
    public static final int TYPE_TOTAL = 6;
    public static final int TYPE_NOTICE = 7;
    public static final int TYPE_BUTTON = 8;

}

        ซึ่ง Type ตรงนี้ก็คือ Type ที่จะใช้อ้างอิงใน Recycler View นั่นเอง

เตรียม Resource ให้พร้อม

Color Resource

        ตั้งชื่อสีให้เวอร์ๆไว้ก่อน เหมือนที่มือถือหลายๆยี่ห้อชอบทำกัน

colors.xml
<?xml version="1.0" encoding="utf-8"?>
<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 ต่างๆเตรียมพร้อมแล้ว มาสร้าง View Holder ให้กับ Adapter กันต่อเลย

สร้าง View Holder และ Layout XML สำหรับ View ทุกแบบ

       ก่อนจะสร้าง Adapter ก็ควรจะสร้าง View Holder ให้พร้อมเสียก่อนเนอะ ทั้ง Layout XML และ View Holder Class สำหรับ View ทั้ง 9 แบบเลย

User Detail ViewHolder/Layout


view_user_detail.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/margin_padding_small"
    android:layout_marginEnd="@dimen/margin_padding"
    android:layout_marginStart="@dimen/margin_padding"
    android:layout_marginTop="@dimen/margin_padding_small"
    android:background="@color/angel_white"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/margin_padding"
    android:paddingEnd="@dimen/margin_padding_large"
    android:paddingStart="@dimen/margin_padding_large"
    android:paddingTop="@dimen/margin_padding">

    <ImageView
        android:layout_width="@dimen/user_icon_size"
        android:layout_height="@dimen/user_icon_size"
        android:layout_marginEnd="@dimen/margin_padding"
        android:contentDescription="@null"
        android:src="@drawable/ic_user_black" />

    <TextView
        android:id="@+id/tv_user_name"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="@color/jet_black"
        android:textSize="@dimen/text_size" />

</LinearLayout>

UserDetailViewHolder.java
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.TextView;


public class UserDetailViewHolder extends RecyclerView.ViewHolder {
    public TextView tvUserName;

    public UserDetailViewHolder(View itemView) {
        super(itemView);
        tvUserName = (TextView) itemView.findViewById(R.id.tv_user_name);
    }
}

Title ViewHolder/Layout


view_title.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/margin_padding_small"
    android:layout_marginEnd="@dimen/margin_padding"
    android:layout_marginStart="@dimen/margin_padding"
    android:layout_marginTop="@dimen/margin_padding_small"
    android:background="@color/supreme_blue"
    android:orientation="horizontal"
    android:padding="@dimen/margin_padding">

    <TextView
        android:id="@+id/tv_title"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:gravity="center"
        android:textColor="@color/angel_white"
        android:textSize="@dimen/text_size" />

</LinearLayout>

TitleViewHolder.java
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.TextView;


public class TitleViewHolder extends RecyclerView.ViewHolder {
    public TextView tvTitle;

    public TitleViewHolder(View itemView) {
        super(itemView);
        tvTitle = (TextView) itemView.findViewById(R.id.tv_title);
    }
}

Section ViewHolder/Layout


view_section.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/margin_padding_small"
    android:layout_marginEnd="@dimen/margin_padding"
    android:layout_marginStart="@dimen/margin_padding"
    android:layout_marginTop="@dimen/margin_padding_small"
    android:background="@color/sky_light_blue"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/margin_padding_small"
    android:paddingEnd="@dimen/margin_padding_large"
    android:paddingStart="@dimen/margin_padding_large"
    android:paddingTop="@dimen/margin_padding_small">

    <TextView
        android:id="@+id/tv_section"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="@color/angel_white"
        android:textSize="@dimen/text_size" />

</LinearLayout>

SectionViewHolder.java
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.TextView;


public class SectionViewHolder extends RecyclerView.ViewHolder {
    public TextView tvSection;

    public SectionViewHolder(View itemView) {
        super(itemView);
        tvSection = (TextView) itemView.findViewById(R.id.tv_section);
    }
}

Order ViewHolder/Layout


view_order.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/margin_padding_small"
    android:layout_marginEnd="@dimen/margin_padding"
    android:layout_marginStart="@dimen/margin_padding"
    android:layout_marginTop="@dimen/margin_padding_small"
    android:background="@color/angel_white"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/margin_padding"
    android:paddingEnd="@dimen/margin_padding_large"
    android:paddingStart="@dimen/margin_padding_large"
    android:paddingTop="@dimen/margin_padding">

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="@dimen/margin_padding"
        android:layout_weight="1"
        android:orientation="vertical">

        <TextView
            android:id="@+id/tv_order_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@color/jet_black"
            android:textSize="@dimen/text_size" />

        <TextView
            android:id="@+id/tv_order_description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textColor="@color/space_gray"
            android:textSize="@dimen/text_size_small" />
    </LinearLayout>

    <TextView
        android:id="@+id/tv_order_price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/seasonal_orange"
        android:textSize="@dimen/text_size_extra_large" />

</LinearLayout>

OrderViewHolder.java
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.TextView;


public class OrderViewHolder extends RecyclerView.ViewHolder {
    public TextView tvOrderName;
    public TextView tvOrderDetail;
    public TextView tvOrderPrice;

    public OrderViewHolder(View itemView) {
        super(itemView);
        tvOrderName = (TextView) itemView.findViewById(R.id.tv_order_name);
        tvOrderDetail = (TextView) itemView.findViewById(R.id.tv_order_detail);
        tvOrderPrice = (TextView) itemView.findViewById(R.id.tv_order_price);
    }
}

Summary ViewHolder/Layout


view_summary.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/margin_padding_small"
    android:layout_marginEnd="@dimen/margin_padding"
    android:layout_marginStart="@dimen/margin_padding"
    android:layout_marginTop="@dimen/margin_padding_small"
    android:background="@color/angel_white"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/margin_padding"
    android:paddingEnd="@dimen/margin_padding_large"
    android:paddingStart="@dimen/margin_padding_large"
    android:paddingTop="@dimen/margin_padding">

    <TextView
        android:id="@+id/tv_summary_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="@dimen/margin_padding"
        android:layout_weight="1"
        android:textColor="@color/jet_black"
        android:textSize="@dimen/text_size" />

    <TextView
        android:id="@+id/tv_summary_price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/seasonal_orange"
        android:textSize="@dimen/text_size_extra_large" />

</LinearLayout>

SummaryViewHolder.java
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.TextView;


public class SummaryViewHolder extends RecyclerView.ViewHolder {
    public TextView tvSummaryName;
    public TextView tvSummaryPrice;

    public SummaryViewHolder(View itemView) {
        super(itemView);
        tvSummaryName = (TextView) itemView.findViewById(R.id.tv_summary_name);
        tvSummaryPrice = (TextView) itemView.findViewById(R.id.tv_summary_price);
    }
}

Total ViewHolder/Layout


view_total.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/margin_padding_small"
    android:layout_marginEnd="@dimen/margin_padding"
    android:layout_marginStart="@dimen/margin_padding"
    android:layout_marginTop="@dimen/margin_padding_small"
    android:background="@color/angel_white"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/margin_padding"
    android:paddingEnd="@dimen/margin_padding_large"
    android:paddingStart="@dimen/margin_padding_large"
    android:paddingTop="@dimen/margin_padding">

    <TextView
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginEnd="@dimen/margin_padding"
        android:layout_weight="1"
        android:text="@string/total"
        android:textColor="@color/soft_red"
        android:textSize="@dimen/text_size_extra_large" />

    <TextView
        android:id="@+id/tv_total_price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:textColor="@color/soft_red"
        android:textSize="@dimen/text_size_extra_large" />

</LinearLayout>

TotalViewHolder.java
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.TextView;


public class TotalViewHolder extends RecyclerView.ViewHolder {
    public TextView tvTotalPrice;

    public TotalViewHolder(View itemView) {
        super(itemView);
        tvTotalPrice = (TextView) itemView.findViewById(R.id.tv_total_price);
    }
}

Notice ViewHolder/Layout


view_notice.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/margin_padding_small"
    android:layout_marginEnd="@dimen/margin_padding"
    android:layout_marginStart="@dimen/margin_padding"
    android:layout_marginTop="@dimen/margin_padding_small"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/margin_padding"
    android:paddingEnd="@dimen/margin_padding_large"
    android:paddingStart="@dimen/margin_padding_large"
    android:paddingTop="@dimen/margin_padding">

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/order_notice"
        android:textColor="@color/space_gray"
        android:textSize="@dimen/text_size_extra_small" />

</LinearLayout>

NoticeViewHolder.java
import android.support.v7.widget.RecyclerView;
import android.view.View;


public class NoticeViewHolder extends RecyclerView.ViewHolder {
    public NoticeViewHolder(View itemView) {
        super(itemView);
    }
}

Button ViewHolder/Layout


view_button.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginBottom="@dimen/margin_padding_small"
    android:layout_marginEnd="@dimen/margin_padding"
    android:layout_marginStart="@dimen/margin_padding"
    android:layout_marginTop="@dimen/margin_padding_small"
    android:gravity="center"
    android:orientation="horizontal"
    android:paddingBottom="@dimen/margin_padding"
    android:paddingEnd="@dimen/margin_padding_large"
    android:paddingStart="@dimen/margin_padding_large"
    android:paddingTop="@dimen/margin_padding">

    <Button
        android:id="@+id/btn_negative"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_marginEnd="@dimen/margin_padding"
        android:background="@drawable/selector_button_negative"
        android:minWidth="@dimen/min_button_width"
        android:text="@string/cancel"
        android:textAllCaps="false"
        android:textColor="@color/angel_white"
        android:textSize="@dimen/text_size" />

    <Button
        android:id="@+id/btn_positive"
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:layout_marginStart="@dimen/margin_padding"
        android:background="@drawable/selector_button_positive"
        android:minWidth="@dimen/min_button_width"
        android:text="@string/confirm"
        android:textAllCaps="false"
        android:textColor="@color/angel_white"
        android:textSize="@dimen/text_size" />

</LinearLayout>

ButtonViewHolder.java
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;


public class ButtonViewHolder extends RecyclerView.ViewHolder {
    public Button btnPositive;
    public Button btnNegative;

    public ButtonViewHolder(View itemView) {
        super(itemView);
        btnPositive = (Button) itemView.findViewById(R.id.btn_positive);
        btnNegative = (Button) itemView.findViewById(R.id.btn_negative);
    }
}

Empty ViewHolder/Layout


view_empty.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="@dimen/empty_space_height" />

EmptyViewHolder.java
import android.support.v7.widget.RecyclerView;
import android.view.View;


public class EmptyViewHolder extends RecyclerView.ViewHolder {
    public EmptyViewHolder(View itemView) {
        super(itemView);
    }
}

        เย้ เตรียมเสร็จทั้งหมดซะที



สร้าง Adapter เปล่าๆเตรียมไว้ก่อน

        เนื่องจากมีบางอย่างที่จะต้องสร้างเพื่อใช้งานใน Adapter แต่เพื่อให้ Adapter พร้อมใช้งานชั่วคราวระหว่างสร้างบางอย่างที่ว่า ดังนั้นตอนนี้จึงขอสร้างโครง Adapter เปล่าๆเตรียมไว้ก่อน

OrderAdapter.java
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;


public class OrderAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return null;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {

    }

    @Override
    public int getItemCount() {
        return 0;
    }
}

        ประมาณนี้ก็พอ เดี๋ยวค่อยมาเพิ่มโค้ดอย่างอื่นทีหลัง

มามะ สร้าง Model Class สำหรับ View Holder กันต่อ

        มาถึงตรงนี้ ผู้ที่หลงเข้ามาอ่านอาจจะสงสัยและแปลกใจว่าทำไมเจ้าของบล็อกถึงสร้าง Model Class สำหรับ View Holder ด้วยล่ะ ทั้งๆที่สร้าง Model Class ที่ชื่อว่า OrderDetail ไปแล้ว แล้วทำไมไม่ใช้คลาสตัวดังกล่าวเพื่อ Map ข้อมูลเข้ากับ Recycler View ไปเลยล่ะ

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


        ซึ่งการโยนคลาส OrderDetail เข้ามาใน Adapter โดยตรงนั้นจะทำให้เจ้าของบล็อกต้องไปเขียน Logic ดังกล่าวอยู่ใน Adapter ไปด้วย ซึ่งคงไม่ค่อยโอเคเท่าไร เพราะจะทำให้ Adapter ตัวนั้นมี Logic ข้างในที่ซับซ้อนมากขึ้นไปอีก (การสร้าง Adapter ที่มี Logic หยุบหยับอยู่ข้างในคงไม่ใช่เรื่องสนุกซักเท่าไร โดยเฉพาะเพื่อนในทีมที่ต้องเข้ามาดูโค้ดส่วนนี้) ดังนั้นเจ้าของบล็อกจึงต้องสร้าง Model Class สำหรับ Adapter ขึ้นมาอีกชุด เพื่อให้ข้อมูลใน Adapter อยู่ในรูปแบบที่กระชับและดูได้ง่ายที่สุดเท่าที่เป็นไปได้ แล้วแปลงข้อมูลจากคลาส OrderDetail เอาทีหลังดีกว่า


สร้าง Base Class สำหรับ Model Class ก่อน

        เนื่องจาก Model Class ของ Adapter นั้นเป็นสิ่งที่เจ้าของบล็อกกำหนดขึ้นมาเอง ไม่เหมือน View Holder ที่มีคลาส ViewHolder เป็น Base Class ให้เอาไป Extend ต่อได้ทันที ดังนั้นจึงต้องสร้าง Base Class ก่อน เพื่อนำไปสร้างเป็น Model Class สำหรับ View Holder แต่ละตัว

BaseOrderDetailItem.java
public class BaseOrderDetailItem {
    private int type;

    public BaseOrderDetailItem(int type) {
        this.type = type;
    }

    public int getType() {
        return type;
    }
}

        เพื่อให้สามารถแยก Model Class แต่ละตัวออกจากกันได้ง่าย จึงใช้วิธีให้ Model Class ทุกตัวนั้นมีค่า Type แตกต่างกัน โดยค่า Type ก็จะกำหนดจากคลาส OrderDetailType ที่สร้างขึ้นไว้ตอนแรกสุดนั้นเอง (ยังจำได้อยู่มั้ยเนี่ย..)

        และเนื่องจาก View Holder แต่ละตัวมีการกำหนดค่าคนละแบบ ดังนั้น Model Class ก็จะมีการเก็บค่าไว้แตกต่างกันออกไปตามแต่ละ View Holder ด้วยเช่นกัน โดยจะมี Getter/Setter สำหรับข้อมูลทุกตัวด้วย (แต่ในตัวอย่างข้างล่างนี้จะไม่ใส่ไว้ เพื่อให้ตัวอย่างโค้ดไม่ยาวจนเกินไป)

User Detail Item

        Model Class ตัวนี้มีหน้าที่แค่เก็บข้อมูลชื่อผู้ใช้งานเท่านั้น

UserDetailItem.java
public class UserDetailItem extends BaseOrderDetailItem {
    private String name;

    public UserDetailItem() {
        super(OrderDetailType.TYPE_USER_DETAIL);
    }

    ...
}

        จะสังเกตเห็นว่า Constructor ของคลาสตัวนี้มีการกำหนดค่าเป็น TYPE_USER_DETAIL ไว้ในตัวเลย ซึ่งคลาสตัวอื่นๆก็จะกำหนดในลักษณะแบบนี้เช่นกัน

Title Item

        Model Class ตัวนี้ทำหน้าที่เก็บข้อมูลแค่ชื่อ Title เท่านั้น

TitleItem.java
public class TitleItem extends BaseOrderDetailItem {
    private String title;

    public TitleItem() {
        super(OrderDetailType.TYPE_TITLE);
    }

    ...
}

Section Item

        Model Class ตัวนี้ทำหน้าที่เก็บข้อมูลชื่อหัวข้อของสินค้าแต่ละกลุ่ม (Section)

SectionItem.java
public class SectionItem extends BaseOrderDetailItem {
    private String section;

    public SectionItem() {
        super(OrderDetailType.TYPE_SECTION);
    }

    ...
}

Order Item

        Model Class ตัวนี้ทำหน้าที่เก็บข้อมูลรายการสินค้า ซึ่งจะแสดงข้อมูล 3 อย่างด้วยกัน ช่ือสินค้า, ข้อมูลเพิ่มเติมของสินค้า และราคา

OrderItem.java
public class OrderItem extends BaseOrderDetailItem {
    private String name;
    private String detail;
    private String price;

    public OrderItem() {
        super(OrderDetailType.TYPE_ORDER);
    }

    ...
}

Summary Item

        Model Class ตัวนี้ทำหน้าที่เก็บประเภทของสินค้าและราคารวมทั้งหมดของสินค้าประเภทนั้นๆ

SummaryItem.java
public class SummaryItem extends BaseOrderDetailItem {
    private String name;
    private String price;

    public SummaryItem() {
        super(OrderDetailType.TYPE_SUMMARY);
    }

    ...
}

Total Item

        Model Class ตัวนี้ทำหน้าที่เก็บราคารวมของสินค้าทั้งหมด

TotalItem.java
public class TotalItem extends BaseOrderDetailItem {
    private String totalPrice;

    public TotalItem() {
        super(OrderDetailType.TYPE_TOTAL);
    }

    public String getTotalPrice() {
        return totalPrice;
    }

    public void setTotalPrice(String totalPrice) {
        this.totalPrice = totalPrice;
    }
}

Notice Item

        Model Class ตัวนี้ไม่ได้มีหน้าที่เก็บข้อมูลใดๆ แค่มีไว้บอกว่าเป็น Notice Type เท่านั้น

NoticeItem.java
public class NoticeItem extends BaseOrderDetailItem {
    public NoticeItem() {
        super(OrderDetailType.TYPE_NOTICE);
    }
}

Button Item

        Model Class ตัวนี้ทำหน้าที่บอกแค่ว่าเป็น Button Type เท่านั้น แต่สามารถปรับให้เก็บชื่อของปุ่ม Negative และ Positive ก็ได้เช่นกัน แต่ในตัวอย่างนี้ปุ่มทั้งสองไม่ต้องเปลี่ยนข้อความ ดังนั้นเจ้าของบล็อกจึงใช้วิธีกำหนดลงไปใน Layout XML โดยตรง

ButtonItem.java
public class ButtonItem extends BaseOrderDetailItem {
    public ButtonItem() {
        super(OrderDetailType.TYPE_BUTTON);
    }
}

Empty Item

        Model Class ตัวนี้ทำหน้าที่บอกแค่ว่าเป็น Empty Type เท่านั้น ไม่ได้เก็บข้อมูลอะไรไว้

EmptyItem.java
public class EmptyItem extends BaseOrderDetailItem {
    public EmptyItem() {
        super(OrderDetailType.TYPE_EMPTY);
    }
}

        ครบทุกตัวแล้ววววว


        ทีนี้ก็เหลือ Converter...

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

        ตามไปอ่านตอนที่ 2 กันได้ที่ [Android Code] ว่าด้วยเรื่อง Recycler View กับการใช้งานจริงในแบบฉบับเจ้าของบล็อก ตอนที่ 2




เหล่าพันธมิตรแอนดรอยด์

Devahoy Layer Net NuuNeoI The Cheese Factory Somkiat CC Mart Routine Artit-K Arnondora Kamonway Try to be android developer Oatrice Benz Nest Studios Kotchaphan@Medium Jirawatee@Medium Travispea@Medium