29 October 2016

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

Updated on


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

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

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


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

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

MainActivity.java
...

private void callService() {
    FakeNetwork.getFakeOrderDetail(new FakeNetwork.OnResultCallback() {
        @Override
        public void onOrderDetailCallback(OrderDetail orderDetail) {
            setOrderDetail(orderDetail);
        }
    });
}

private void setOrderDetail(OrderDetail orderDetail) {
    // TODO Render orderDetail to Recycler View
}

        ซึ่งผลลัพธ์ที่ได้จะเข้าไปที่ setOrderDetail ซึ่งในนั้นเจ้าของบล็อกจะแปลงข้อมูลแล้วส่งให้ Adapter เพื่อแสดงผลใน Recycler View

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

        อย่างที่บอกในตอนแรกว่าการเอาข้อมูลดิบไปใส่ไว้ใน Adapter โดยตรงคงไม่ใช่เรื่องสนุกซักเท่าไร โดยเฉพาะโค้ดที่ต้องมีการดูแลกันในระยะยาว เพราะวันหนึ่งเมื่อกลับมาดูก็อาจจะลืมไปแล้วก็ได้ว่าข้างในนั้นทำอะไรบ้าง ดังนั้นจึงต้องการทำงานออกเป็นส่วนๆ ถึงแม้ว่าจะมีคำสั่งเพิ่มขึ้นก็ตาม แต่ถ้ามันทำให้อ่านแล้วเข้าใจง่ายขึ้นก็ควรทำนะ
        โดย Converter จะต้องแปลงข้อมูลดิบที่อยู่ในรูปของคลาสให้กลายเป็นคลาส BaseOrderDetailItem ที่อยู่ในรูปของ List (หรือ Array น่ะแหละ) เพื่อนำไปแสดงผลใน Recycler View นั่นเอง
        ทีนี้ขอย้อนความทรงจำก่อนว่า Recycler View ที่ลูกค้าต้องการเนี่ย มันจะมีส่วนของข้อมูลที่ Fixed ตำแหน่งไว้ และข้อมูลอีกส่วนคือข้อมูล Dynamic ที่มีจำนวนไม่แน่นอน ซึ่งมันดันอยู่แทรกกันไปมาแบบนี้

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

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

        อ้าว...
        นั่นก็เพราะว่าอยากให้มองเป็นคำสั่งคร่าวๆแบบนี้ก่อน

MainActivity.java
private RecyclerView rvOrderDetail;
private OrderAdapter orderAdapter;

...

private void setOrderDetail(OrderDetail orderDetail) {
    String name = "Sleeping For Less";
    String yourOrderTitle = getString(R.string.your_order);
    String summaryTitle = getString(R.string.summary);

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

    List<BaseOrderDetailItem> orderDetailItemList = new ArrayList<>();
    orderDetailItemList.add(OrderDetailConverter.createUserDetail(name));
    orderDetailItemList.add(OrderDetailConverter.createTitle(yourOrderTitle));
//  orderDetailItemList.addAll(OrderDetailConverter.createSectionAndOrder(orderDetail, foodTitle, bookTitle, musicTitle, currency));
    orderDetailItemList.add(OrderDetailConverter.createTitle(summaryTitle));
//  orderDetailItemList.addAll(OrderDetailConverter.createSummary(orderDetail, foodTitle, bookTitle, musicTitle, currency));
    orderDetailItemList.add(OrderDetailConverter.createTotal(orderDetail, currency));
    orderDetailItemList.add(OrderDetailConverter.createNotice());
    orderDetailItemList.add(OrderDetailConverter.createButton());
    orderDetailItemList.add(OrderDetailConverter.createEmpty());

    orderAdapter = new OrderAdapter();
    orderAdapter.setOrderItemList(orderDetailItemList);
    rvOrderDetail.setAdapter(orderAdapter);
}

        อันนี้คือรูปแบบคำสั่งที่คิดไว้คร่าวๆ ซึ่งเจ้าของบล็อกยังไม่ได้เขียนขึ้นมา โดยจะสร้างคลาส OrderDetailConverter ขึ้นมาเพื่อแปลงข้อมูลให้กลายเป็น Model ที่ต้องการ โดยเก็บไว้รวมกันใน List ของ BaseOrderDetailItem โดยส่วนที่เป็นข้อมูล Dynamic จะใส่ Comment ไว้ชั่วคราวก่อน

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

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

OrderDetailConverter.java
import java.util.List;


public class OrderDetailConverter {

    public static UserDetailItem createUserDetail(String name) {
        // Do something
        return null;
    }

    public static TitleItem createTitle(String yourOrderTitle) {
        // Do something
        return null;
    }

    public static TotalItem createTotal(OrderDetail orderDetail) {
        // Do something
        return null;
    }

    public static NoticeItem createNotice() {
        // Do something
        return null;
    }

    public static ButtonItem createButton() {
        // Do something
        return null;
    }

    public static EmptyItem createEmpty() {
        // Do something
        return null;
    }

    public static List<BaseOrderDetailItem> createSectionAndOrder(OrderDetail orderDetail) {
        // Do something
        return null;
    }

    public static List<SummaryItem> createSummary(OrderDetail orderDetail) {
        // Do something
        return null;
    }
}

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

คำสั่ง createUserDetail 

        จะเห็นว่าคำสั่งนี้ต้องมีการส่งชื่อที่อยู่ในรูป String เข้ามาด้วย เพราะจะเอาไปเก็บไว้ใน UserDetailItem นั่นเอง

OrderDetailConverter.java
...

public static UserDetailItem createUserDetail(String name) {
    UserDetailItem userDetailItem = new UserDetailItem();
    userDetailItem.setName(name);
    return userDetailItem;
}

คำสั่ง createTitle

OrderDetailConverter.java
...

public static TitleItem createTitle(String yourOrderTitle) {
    TitleItem titleItem = new TitleItem();
    titleItem.setTitle(yourOrderTitle);
    return titleItem;
}

คำสั่ง createTotal

        โค้ดชุดนี้จะเยอะหน่อย เนื่องจากต้องคำนวณรวมผลลัพธ์ของรายการทั้งหมด โดยหน่วยเงินจะไม่ได้กำหนดในคำสั่งนี้โดยตรง แต่จะให้ส่งเข้ามาเมื่อเรียก Method นี้แทน

OrderDetailConverter.java
...

public static TotalItem createTotal(OrderDetail orderDetail, String currency) {
    TotalItem totalItem = new TotalItem();
    totalItem.setTotalPrice(getTotalPrice(orderDetail) + currency);
    return totalItem;
}

private static int getTotalPrice(OrderDetail orderDetail) {
    int totalPrice = 0;
    totalPrice += getTotalFoodPrice(orderDetail.getFoodList());
    totalPrice += getTotalBookPrice(orderDetail.getBookList());
    totalPrice += getTotalMusicPrice(orderDetail.getMusicList());
    return totalPrice;
}

private static int getTotalFoodPrice(List<OrderDetail.Food> foodList) {
    int totalFoodPrice = 0;
    if (foodList != null) {
        for (OrderDetail.Food food : foodList) {
            totalFoodPrice += food.getPrice();
        }
    }
    return totalFoodPrice;
}

private static int getTotalBookPrice(List<OrderDetail.Book> bookList) {
    int totalBookPrice = 0;
    if (bookList != null) {
        for (OrderDetail.Book book : bookList) {
            totalBookPrice += book.getPrice();
        }
    }
    return totalBookPrice;
}

private static int getTotalMusicPrice(List<OrderDetail.Music> musicList) {
    int totalMusicPrice = 0;
    if (musicList != null) {
        for (OrderDetail.Music music : musicList) {
            totalMusicPrice += music.getPrice();
        }
    }
    return totalMusicPrice;
}

คำสั่ง createNotice

        เนื่องจาก Notice Type ไม่มีการกำหนดข้อมูลเข้าไป ดังนั้นแค่สร้าง Instance ขึ้นมาเฉยๆก็พอ

OrderDetailConverter.java
...

public static NoticeItem createNotice() {
    return new NoticeItem();
}

คำสั่ง createButton

        จริงๆแล้วจะต้องกำหนด Listener ของปุ่มได้ด้วย แต่ไม่ได้กำหนดจากในนี้ ซึ่งเดี๋ยวจะพูดถึงในทีหลัง ดังนั้นใน Button Type ก็แค่สร้าง Instance เปล่าๆขึ้นมาอย่างเดียว

OrderDetailConverter.java
...

public static ButtonItem createButton() {
    return new ButtonItem();
}

คำสั่ง createEmpty

OrderDetailConverter.java
...

public static EmptyItem createEmpty() {
    return new EmptyItem();
}


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

คำสั่ง createSectionAndOrder

        ถ้าสังเกตจากคำสั่งที่เจ้าของบล็อกเตรียมไว้จะเห็นว่า Return Type เป็น BaseOrderDetailItem ทั้งนี้ก็เพราะว่าข้อมูลส่วนนี้จะมีทั้ง SectionItem และ OrderItem ผสมกัน ดังนั้นเพื่อให้ข้อมูลทั้งสองแบบเก็บรวมกันไว้ใน List ตัวเดียวกันได้ ก็ต้องใช้ Base Class แบบนี้น่ะแหละ

OrderDetailConverter.java
public static List<BaseOrderDetailItem> createSectionAndOrder(OrderDetail orderDetail, String foodTitle, String bookTitle, String musicTitle, String currency) {
    List<BaseOrderDetailItem> orderDetailItemList = new ArrayList<>();
    orderDetailItemList.addAll(getFoodOrderDetailList(orderDetail.getFoodList(), foodTitle, currency));
    orderDetailItemList.addAll(getBookOrderDetailList(orderDetail.getBookList(), bookTitle, currency));
    orderDetailItemList.addAll(getMusicOrderDetailList(orderDetail.getMusicList(), musicTitle, currency));
    return orderDetailItemList;
}

private static List<BaseOrderDetailItem> getFoodOrderDetailList(List<OrderDetail.Food> foodList, String foodTitle, String currency) {
    List<BaseOrderDetailItem> foodOrderDetailList = new ArrayList<>();
    if (foodList != null && foodList.size() > 0) {
        foodOrderDetailList.add(createSection(foodTitle));
        for (OrderDetail.Food food : foodList) {
            String name = food.getOrderName();
            String detail = "x" + food.getAmount();
            String price = food.getPrice() + currency;
            foodOrderDetailList.add(createOrder(name, detail, price));
        }
    }
    return foodOrderDetailList;
}

private static List<BaseOrderDetailItem> getBookOrderDetailList(List<OrderDetail.Book> bookList, String bookTitle, String currency) {
    List<BaseOrderDetailItem> bookOrderDetailList = new ArrayList<>();
    if (bookList != null && bookList.size() > 0) {
        bookOrderDetailList.add(createSection(bookTitle));
        for (OrderDetail.Book book : bookList) {
            String name = book.getBookName();
            String detail = book.getAuthor();
            String price = book.getPrice() + currency;
            bookOrderDetailList.add(createOrder(name, detail, price));
        }
    }
    return bookOrderDetailList;
}

private static List<BaseOrderDetailItem> getMusicOrderDetailList(List<OrderDetail.Music> musicList, String musicTitle, String currency) {
    List<BaseOrderDetailItem> musicOrderDetailList = new ArrayList<>();
    if (musicList != null && musicList.size() > 0) {
        musicOrderDetailList.add(createSection(musicTitle));
        for (OrderDetail.Music music : musicList) {
            String name = music.getAlbum();
            String detail = music.getArtist();
            String price = music.getPrice() + currency;
            musicOrderDetailList.add(createOrder(name, detail, price));
        }
    }
    return musicOrderDetailList;
}

private static SectionItem createSection(String title) {
    SectionItem sectionItem = new SectionItem();
    sectionItem.setSection(title);
    return sectionItem;
}

private static OrderItem createOrder(String name, String detail, String price) {
    OrderItem orderItem = new OrderItem();
    orderItem.setName(name);
    orderItem.setDetail(detail);
    orderItem.setPrice(price);
    return orderItem;
}

        คำสั่งยืดยาวไปหน่อย ทั้งนี้ก็เพราะแยกคำสั่งตามหน้าที่ให้ย่อยที่สุดเท่าที่ทำได้ ซึ่งจะเห็นว่าชื่อประเภทของรายการสินค้า (Section) นั้นก็จะมีการโยนข้อมูลเข้ามา เพราะว่าชื่อประเภทเหล่านั้นถูกเก็บไว้เป็น String Resource (กลับไปดูได้ในตอนที่ 1) ซึ่งเจ้าของบล็อกอยากให้คลาส OrderConverter เป็นคำสั่ง Plain Java ทั้งหมด ดังนั้นจึงไม่มีพวก Context หรือ R.string.xxxx อยู่ในนี้ จะใช้วิธีโยนค่าเข้ามาในตอนเรียกใช้งานทั้งหมด

        และถึงแม้ว่าข้อมูลสินค้าทั้ง 3 แบบ (Food, Book และ Music) จะแสดงผลเป็น Order Type เหมือนกัน แต่ทว่าข้อมูลที่เอามาแสดงนั้นแตกต่างกัน จึงต้องมีการสร้าง Method แยกออกเป็น 3 ชุดด้วยกันเพื่อให้จัดการแยกของใครของมัน

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

คำสั่ง createSummary

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

OrderDetailConverter.java
public static List<SummaryItem> createSummary(OrderDetail orderDetail, String foodTitle, String bookTitle, String musicTitle, String currency) {
    List<SummaryItem> summaryItemList = new ArrayList<>();
    if (orderDetail != null) {
        summaryItemList.addAll(getFoodSummary(orderDetail.getFoodList(), foodTitle, currency));
        summaryItemList.addAll(getBookSummary(orderDetail.getBookList(), bookTitle, currency));
        summaryItemList.addAll(getMusicSummary(orderDetail.getMusicList(), musicTitle, currency));
    }
    return summaryItemList;
}

private static List<SummaryItem> getFoodSummary(List<OrderDetail.Food> foodList, String foodTitle, String currency) {
    List<SummaryItem> foodSummaryItemList = new ArrayList<>();
    if (foodList != null && foodList.size() > 0) {
        SummaryItem summaryItem = new SummaryItem();
        summaryItem.setName(foodTitle);
        summaryItem.setPrice(getTotalFoodPrice(foodList) + currency);
        foodSummaryItemList.add(summaryItem);
    }
    return foodSummaryItemList;
}

private static List<SummaryItem> getBookSummary(List<OrderDetail.Book> bookList, String bookTitle, String currency) {
    List<SummaryItem> bookSummaryItemList = new ArrayList<>();
    if (bookList != null && bookList.size() > 0) {
        SummaryItem summaryItem = new SummaryItem();
        summaryItem.setName(bookTitle);
        summaryItem.setPrice(getTotalBookPrice(bookList) + currency);
        bookSummaryItemList.add(summaryItem);
    }
    return bookSummaryItemList;
}

private static List<SummaryItem> getMusicSummary(List<OrderDetail.Music> musicList, String musicTitle, String currency) {
    List<SummaryItem> bookSummaryItemList = new ArrayList<>();
    if (musicList != null && musicList.size() > 0) {
        SummaryItem summaryItem = new SummaryItem();
        summaryItem.setName(musicTitle);
        summaryItem.setPrice(getTotalMusicPrice(musicList) + currency);
        bookSummaryItemList.add(summaryItem);
    }
    return bookSummaryItemList;
}

        สำหรับผู้ที่หลงเข้ามาอ่านคนใดสงสัยว่าคำสั่ง getTotalFoodPrice, getTotalBookPrice และ getTotalMusicPrice นั้นโผล่มาจากไหน ทำไมไม่เห็นสร้างขึ้นมาเลย จริงๆแล้วคำสั่งนี้ถูกสร้างขึ้นตั้งแต่ตอนเพิ่มโค้ดในคำสั่ง createTotal แล้วนะ และเนื่องจากคำสั่งดังกล่าวทำงานเหมือนกัน ดังนั้นจึงหยิบมาใช้ต่อได้เลย

        และจะสังเกตเห็นว่าพวกคำสั่ง getFoodSummary, getBookSummary และ getMusicSummary ถูกกำหนด Return Type เป็น List<SummaryItem> ทั้งหมดเลย ทั้งๆที่ตัวมันสามารถ Return ออกมาเป็น SummaryItem ได้เลย ไม่จำเป็นต้องทำเป็น List ซักหน่อย แล้วทำไมเจ้าของบล็อกต้องทำให้ยุ่งยากโดยการทำให้มันเป็น List ก่อนส่งออกมากันล่ะ?

      นั่นก็เพราะว่าถ้าส่งออกมาเป็น SummaryItem จะต้องเขียนแบบนี้

OrderDetailConverter.java
private static SummaryItem getFoodSummary(List<OrderDetail.Food> foodList, String foodTitle, String currency) {
    if (foodList != null && foodList.size() > 0) {
        SummaryItem summaryItem = new SummaryItem();
        summaryItem.setName(foodTitle);
        summaryItem.setPrice(getTotalFoodPrice(foodList) + currency);
        return summaryItem;
    }
    return null;
}

        จะเห็นว่าผลลัพธ์มีโอกาสเป็น Null ได้ ถ้าไม่มีข้อมูลอยู่ข้างใน ดังนั้นเมื่อส่ง Null ออกมา ก็ต้องมานั่งเช็คที่ปลายทางอีกว่าผลลัพธ์ที่ได้เป็น Null หรือป่าว เพื่อป้องกันปัญหา NullPointerException ในภายหลัง ดังนั้นจึงใช้วิธีสร้าง List ที่มีขนาด 0 ตัวตั้งแต่แรกเลย เพื่อจะได้ไม่ต้องมานั่งเช็ค Null ให้เสียเวลา แต่ก็ต้องยอมรับแหละว่ามันไม่ใช่วิธีที่ดีเท่าไร แค่เขียนสั้นลงเฉยๆ (ผู้ที่หลงเข้ามาอ่านสามารถเลือกเขียนตามที่คิดว่าเหมาะสมได้เลยครับ)


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

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

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

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/activity_main"
    android:layout_width="match_parent"
    android:background="@color/little_light_gray"
    android:layout_height="match_parent">

    <android.support.v7.widget.RecyclerView
        android:id="@+id/rv_order_detail"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />
        
</RelativeLayout>

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

MainActivity.java
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;

import java.util.ArrayList;
import java.util.List;

public class MainActivity extends AppCompatActivity {
    private RecyclerView rvOrderDetail;
    private OrderAdapter orderAdapter;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        bindView();
        setupView();
        callService();
    }

    private void bindView() {
        rvOrderDetail = (RecyclerView) findViewById(R.id.rv_order_detail);
    }

    private void setupView() {
        rvOrderDetail.setLayoutManager(new LinearLayoutManager(this));
        orderAdapter = new OrderAdapter();
        rvOrderDetail.setAdapter(orderAdapter);
    }

    private void callService() {
        FakeNetwork.getFakeOrderDetail(new FakeNetwork.OnResultCallback() {
            @Override
            public void onOrderDetailCallback(OrderDetail orderDetail) {
                setOrderDetail(orderDetail);
            }
        });
    }

    private void setOrderDetail(OrderDetail orderDetail) {
        String name = "Sleeping For Less";
        String yourOrderTitle = getString(R.string.your_order);
        String summaryTitle = getString(R.string.summary);

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

        List<BaseOrderDetailItem> orderDetailItemList = new ArrayList<>();
        orderDetailItemList.add(OrderDetailConverter.createUserDetail(name));
        orderDetailItemList.add(OrderDetailConverter.createTitle(yourOrderTitle));
        orderDetailItemList.addAll(OrderDetailConverter.createSectionAndOrder(orderDetail, foodTitle, bookTitle, musicTitle, currency));
        orderDetailItemList.add(OrderDetailConverter.createTitle(summaryTitle));
        orderDetailItemList.addAll(OrderDetailConverter.createSummary(orderDetail, foodTitle, bookTitle, musicTitle, currency));
        orderDetailItemList.add(OrderDetailConverter.createTotal(orderDetail, currency));
        orderDetailItemList.add(OrderDetailConverter.createNotice());
        orderDetailItemList.add(OrderDetailConverter.createButton());
        orderDetailItemList.add(OrderDetailConverter.createEmpty());

        // orderAdapter.setOrderItemList(orderDetailItemList);
        // orderAdapter.notifyDataSetChanged();
    }
}

        หัวใจสำคัญของการทำงานอยู่ที่ callService() ที่จะเริ่มทำงานทันทีที่ Activity เริ่มทำงาน กับ setOrderDetail(OrderDetail orderDetail) ที่จะทำงานหลังจากได้ข้อมูล (จำลอง) เพื่อแสดงลงบน Recycler View

        สำหรับ Android Manifest ไม่จำเป็นต้องดูมั้ง... ข้ามมันไปเลยเนอะ

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

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

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

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;
    }
}

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

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

import java.util.ArrayList;
import java.util.List;


public class OrderAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private List<BaseOrderDetailItem> orderDetailItemList;

    public OrderAdapter() {
        orderDetailItemList = new ArrayList<>();
    }

    public void setOrderItemList(List<BaseOrderDetailItem> orderDetailItemList) {
        this.orderDetailItemList = orderDetailItemList;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return null;
    }

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

    }

    @Override
    public int getItemViewType(int position) {
        return orderDetailItemList.get(position).getType();
    }

    @Override
    public int getItemCount() {
        return orderDetailItemList.size();
    }
}

        สิ่งที่เพิ่มเข้าไปคือประกาศ Instance ของ List<BaseOrderDetailItem> และ Override Method ที่ชื่อว่า getItemViewType(int position) ที่ขาดไม่ได้เมื่อต้องทำ View Holder หลายๆแบบ

        โดยปกติผู้ที่หลงเข้ามาอ่านส่วนใหญ่จะโยนข้อมูลเข้ามาทาง Constructure ซึ่งเป็นการกำหนดข้อมูลพร้อมๆกับการสร้าง Adapter ขึ้นมา แต่กรณีของเจ้าของบล็อกจะใช้วิธีสร้าง Adapter ขึ้นมาเตรียมไว้ แล้วค่อยกำหนดข้อมูลทีหลังได้ผ่านคำสั่ง setOrderItemList(List<BaseOrderDetailItem> orderDetailItemList) เพื่อให้ยืดหยุ่นต่อการเพิ่มเติมหรือแก้ไขข้อมูลในทีหลัง
     
        ใน getItemViewType(int position) ก็จะดึง ViewType จาก Type ที่เตรียมไว้ใน Model Class แล้ว (ที่อิงจากคลาส OrderDetailType) และ getItemCount() ก็จะอิงจากจำนวนข้อมูลที่มีอยู่ทั้งหมด

        ซึ่งโค้ดในส่วนนี้สามารถเขียนเพิ่มเพื่อดัก null ได้ตามใจชอบเลย

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

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

OrderAdapter.java
...

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    if (viewType == OrderDetailType.TYPE_USER_DETAIL) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_user_detail, parent, false);
        return new UserDetailViewHolder(view);

    } else if (viewType == OrderDetailType.TYPE_TITLE) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_title, parent, false);
        return new TitleViewHolder(view);

    } else if (viewType == OrderDetailType.TYPE_SECTION) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_section, parent, false);
        return new SectionViewHolder(view);

    } else if (viewType == OrderDetailType.TYPE_ORDER) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_order, parent, false);
        return new OrderViewHolder(view);

    } else if (viewType == OrderDetailType.TYPE_SUMMARY) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_summary, parent, false);
        return new SummaryViewHolder(view);

    } else if (viewType == OrderDetailType.TYPE_TOTAL) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_total, parent, false);
        return new TotalViewHolder(view);

    } else if (viewType == OrderDetailType.TYPE_NOTICE) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_notice, parent, false);
        return new NoticeViewHolder(view);

    } else if (viewType == OrderDetailType.TYPE_BUTTON) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_button, parent, false);
        return new ButtonViewHolder(view);

    } else if (viewType == OrderDetailType.TYPE_EMPTY) {
        View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.view_empty, parent, false);
        return new EmptyViewHolder(view);

    }
    throw new NullPointerException("View Type " + viewType + " doesn't match with any existing order detail type");
}

        ขอเว้นเคาะบรรทัดในแต่ละเงื่อนไขเพื่อให้ดูได้สะดวกมากขึ้น (ถ้าติดกันทุกบรรทัดคงตาลายน่าดู)

        โดยปกติแล้วถ้าไม่ตรงกับเงื่อนไขใดๆมักจะ Return เป็น null กัน แต่เวลาที่เจอปัญหา NullPointerException เพราะเงื่อนไขดังกล่าว มันจะดูใน Stacktrace แล้วหาสาเหตุได้จาก (ถ้าจำไม่ผิดใน Logcat มันไม่ยอมระบุว่ามาจากบรรทัดไหน) ดังนั้นเพื่อให้ชัวร์ที่สุด เจ้าของบล็อกจึงโยน NullPointerException พร้อมกับข้อความที่กำหนดเองซะเลย จะรู้ได้ทันทีว่ามันเป็นเพราะอะไร

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

        ใน onBindViewHolder ก็จะให้นักพัฒนา Binding ข้อมูลเข้ากับ View Holder เพื่อให้แสดงผลตามต้องการ ซึ่งในกรณีของเจ้าของบล็อกที่มี Model และ View Holder หลายแบบ ก็จะได้โค้ดออกมาประมาณนี้

OrderAdapter.java
...

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    BaseOrderDetailItem orderDetailItem = orderDetailItemList.get(position);
    if (holder instanceof UserDetailViewHolder) {
        UserDetailViewHolder userDetailViewHolder = (UserDetailViewHolder) holder;
        UserDetailItem userDetailItem = (UserDetailItem) orderDetailItem;
        setupUserDetail(userDetailViewHolder, userDetailItem);

    } else if (holder instanceof TitleViewHolder) {
        TitleViewHolder titleViewHolder = (TitleViewHolder) holder;
        TitleItem titleItem = (TitleItem) orderDetailItem;
        setupTitle(titleViewHolder, titleItem);

    } else if (holder instanceof SectionViewHolder) {
        SectionViewHolder sectionViewHolder = (SectionViewHolder) holder;
        SectionItem sectionItem = (SectionItem) orderDetailItem;
        setupSection(sectionViewHolder, sectionItem);

    } else if (holder instanceof OrderViewHolder) {
        OrderViewHolder orderViewHolder = (OrderViewHolder) holder;
        OrderItem orderItem = (OrderItem) orderDetailItem;
        setupOrder(orderViewHolder, orderItem);

    } else if (holder instanceof SummaryViewHolder) {
        SummaryViewHolder summaryViewHolder = (SummaryViewHolder) holder;
        SummaryItem summaryItem = (SummaryItem) orderDetailItem;
        setupSummary(summaryViewHolder, summaryItem);

    } else if (holder instanceof TotalViewHolder) {
        TotalViewHolder totalViewHolder = (TotalViewHolder) holder;
        TotalItem totalItem = (TotalItem) orderDetailItem;
        setupTotal(totalViewHolder, totalItem);

    } else if (holder instanceof NoticeViewHolder) {
        NoticeViewHolder noticeViewHolder = (NoticeViewHolder) holder;
        NoticeItem noticeItem = (NoticeItem) orderDetailItem;
        setupNotice(noticeViewHolder, noticeItem);

    } else if (holder instanceof ButtonViewHolder) {
        ButtonViewHolder buttonViewHolder = (ButtonViewHolder) holder;
        ButtonItem buttonItem = (ButtonItem) orderDetailItem;
        setupButton(buttonViewHolder, buttonItem);

    } else if (holder instanceof EmptyViewHolder) {
        EmptyViewHolder emptyViewHolder = (EmptyViewHolder) holder;
        EmptyItem emptyItem = (EmptyItem) orderDetailItem;
        setupEmpty(emptyViewHolder, emptyItem);

    }
}

private void setupUserDetail(UserDetailViewHolder userDetailViewHolder, UserDetailItem userDetailItem) {
    // TODO Do something
}

private void setupTitle(TitleViewHolder titleViewHolder, TitleItem titleItem) {
    // TODO Do something
}

private void setupSection(SectionViewHolder sectionViewHolder, SectionItem sectionItem) {
    // TODO Do something
}

private void setupOrder(OrderViewHolder orderViewHolder, OrderItem orderItem) {
    // TODO Do something
}

private void setupSummary(SummaryViewHolder summaryViewHolder, SummaryItem summaryItem) {
    // TODO Do something
}

private void setupTotal(TotalViewHolder totalViewHolder, TotalItem totalItem) {
    // TODO Do something
}

private void setupNotice(NoticeViewHolder noticeViewHolder, NoticeItem noticeItem) {
    // TODO Do something
}

private void setupButton(ButtonViewHolder buttonViewHolder, ButtonItem buttonItem) {
    // TODO Do something
}

private void setupEmpty(EmptyViewHolder emptyViewHolder, EmptyItem emptyItem) {
    // TODO Do something
}

        ปล. แอบขัดใจโค้ดตรงนี้เล็กน้อย แต่ช่างมันเถอะ...

        อย่าคิดที่จะยัดทุกอย่างไว้ใน onBindViewHolder เด็ดขาด เพราะนั่นจะทำให้โค้ดเบ้อเริ่มที่มีอะไรไม่รู้อยู่ในนั้นเต็มไปหมดเลยนะ โดยเฉพาะอย่างยิ่ง View Holder ที่มีหลายประเภทแบบบทความนี้

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

setupUserDetail

OrderAdapter.java
...

private void setupUserDetail(UserDetailViewHolder userDetailViewHolder, UserDetailItem userDetailItem) {
    userDetailViewHolder.tvUserName.setText(userDetailItem.getName());
}

setupTitle

OrderAdapter.java
...

private void setupTitle(TitleViewHolder titleViewHolder, TitleItem titleItem) {
    titleViewHolder.tvTitle.setText(titleItem.getTitle());
}

setupSection

OrderAdapter.java
...

private void setupSection(SectionViewHolder sectionViewHolder, SectionItem sectionItem) {
    sectionViewHolder.tvSection.setText(sectionItem.getSection());
}

setupOrder

OrderAdapter.java
...

private void setupOrder(OrderViewHolder orderViewHolder, OrderItem orderItem) {
    orderViewHolder.tvOrderName.setText(orderItem.getName());
    orderViewHolder.tvOrderDetail.setText(orderItem.getDetail());
    orderViewHolder.tvOrderPrice.setText(orderItem.getPrice());
}

setupSummary

OrderAdapter.java
...

private void setupSummary(SummaryViewHolder summaryViewHolder, SummaryItem summaryItem) {
    summaryViewHolder.tvSummaryName.setText(summaryItem.getName());
    summaryViewHolder.tvSummaryPrice.setText(summaryItem.getPrice());
}

setupTotal

OrderAdapter.java
...

private void setupTotal(TotalViewHolder totalViewHolder, TotalItem totalItem) {
    totalViewHolder.tvTotalPrice.setText(totalItem.getTotalPrice());
}

setupNotice

OrderAdapter.java
...

private void setupNotice(NoticeViewHolder noticeViewHolder, NoticeItem noticeItem) {
    // Nothing to do ...
}

setupButton

        ที่เคยติดค้างไว้เรื่อง Listener ของ Button ในที่สุดคำตอบของตอนนั้นก็มาถึงแล้วนะจ๊ะ ซึ่ง Listener ของ Button จะถูกกำหนดใน setupButton นั่นเอง

        โดยใน Adapter เจ้าของบล็อกได้เพิ่ม Listener ที่ชื่อว่า OnItemClickListener โดยมี Event สองตัวคือ onPositiveButtonClick() และ onNegativeButtonClick() โดยเอา Listener ตัวดังกล่าวไปกำหนดให้กับ Button View Holder นั่นเอง

OrderAdapter.java
...

private OnItemClickListener onItemClickListener;

public void setOnItemClickListener(OnItemClickListener onItemClickListener) {
    this.onItemClickListener = onItemClickListener;
}

private void setupButton(ButtonViewHolder buttonViewHolder, ButtonItem buttonItem) {
    buttonViewHolder.btnPositive.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (onItemClickListener != null) {
                onItemClickListener.onPositiveButtonClick();
            }
        }
    });
    buttonViewHolder.btnNegative.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if (onItemClickListener != null) {
                onItemClickListener.onNegativeButtonClick();
            }
        }
    });
}

public interface OnItemClickListener {
    void onPositiveButtonClick();

    void onNegativeButtonClick();
}

        ดังนั้นเจ้าของบล็อกจึงเพิ่มคำสั่ง setOnItemClickListener ด้วย เพื่อให้สามารถกำหนด Listener ตัวนี้จาก MainActivity ได้นั่นเอง

setupEmpty

OrderAdapter.java
...

private void setupEmpty(EmptyViewHolder emptyViewHolder, EmptyItem emptyItem) {
    // Nothing to do ...
}

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

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

orderAdapter.setOrderItemList(orderDetailItemList);
orderAdapter.notifyDataSetChanged();

        ตอนนี้คำสั่งนี้ใช้งานได้แล้วจ้า (ก็พึ่งสร้างคำสั่งเมื่อกี้นี้ไง)

MainActivity.java
private void setOrderDetail(OrderDetail orderDetail) {
    ...

    orderAdapter.setOrderItemList(orderDetailItemList);
    orderAdapter.notifyDataSetChanged();
}

        แล้วก็อย่าลืมกำหนด setOnItemClickListener ที่พึ่งสร้างขึ้นเมื่อกี้ด้วยนะ

MainActivity.java
public class MainActivity extends AppCompatActivity implements OrderAdapter.OnItemClickListener {
    ...

    private void setupView() {
        ...

        orderAdapter = new OrderAdapter();
        orderAdapter.setOnItemClickListener(this);

        ...
    }

    ...

    @Override
    public void onPositiveButtonClick() {
        Toast.makeText(this, "Positive Button Clicked", Toast.LENGTH_SHORT).show();
    }

    @Override
    public void onNegativeButtonClick() {
        Toast.makeText(this, "Negative Button Clicked", Toast.LENGTH_SHORT).show();
    }
}

        เอาให้มันแสดง Toast แบบง่ายๆก็พอเนอะ ให้รู้ว่ามันทำงานได้จริงๆก็พอ

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

FakeNetwork.java
import android.os.Handler;

import com.google.gson.Gson;


public class FakeNetwork {
    public static void getFakeOrderDetail(final OnResultCallback onResultCallback) {
        new Handler().postDelayed(new Runnable() {
            @Override
            public void run() {
                OrderDetail orderDetail = createFakeOrderDetail();
                if (onResultCallback != null) {
                    onResultCallback.onOrderDetailCallback(orderDetail);
                }
            }
        }, 1000);
    }

    private static OrderDetail createFakeOrderDetail() {
        String 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 new Gson().fromJson(fakeJson, OrderDetail.class);
    }

    public interface OnResultCallback {
        void onOrderDetailCallback(OrderDetail orderDetail);
    }
}

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

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

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


        จะเห็นว่า Recycler View สามารถทำงานได้ตรงที่ต้องการเป๊ะๆ ถึงแม้ว่า Layout จะไม่ตรงกับที่ดีไซน์ไปบ้าง (ขี้เกียจแก้ไขใหม่แล้วอ่ะ....) ก็ขอให้มองข้ามเรื่องดีไซน์ไปก่อนนะ (อย่างน้อยมันก็เรียงตรง Requirement นะเออ!!)

        เย้ เย้ เย้

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


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

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

        แต่ผู้ที่หลงเข้ามาอ่านบางคนก็คงจะรู้คำตอบแล้วล่ะ...