04 November 2016

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

Updated on


        ก็ไม่คิดว่าจะเขียนบทความซีรีย์ Recycler View in Action ได้ยืดยาวขนาดนี้ (ตอนแรกตั้งใจว่าจะเขียนแค่ 3 บทความ) แต่เนื่องจากเนื้อหามันเริ่มเลยเถิดไปเรื่อยๆตามไอเดียที่เจ้าของบล็อกอยากจะเขียน เพราะงั้นก็ปล่อยให้มันเป็นไปตามยถากรรมก็แล้วกันเนอะ...

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

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

ย้อนความทรงจำ

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


เมื่อเงื่อนไขการทำงานเพิ่มมากขึ้น

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

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

        แต่เพื่อให้สนุกและท้าทายมากขึ้น...

        ลองทำให้ Recycler View ตัวนี้สามารถลบข้อมูลออกไปได้ดีกว่า 

        แล้วมีส่วนไหนของโค้ดที่จะต้องแก้ไขบ้างล่ะเนี่ย?

ลองคิดเล่นๆก่อนที่จะเขียน

        อย่างที่รู้กันว่ารูปแบบนี้ เจ้าของบล็อกไม่อยากให้ Adapter ทำอะไรมากนัก เพราะเดี๋ยวโค้ดในนั้นมันจะซับซ้อนเกินไป ถ้าไม่จำเป็นจริงๆก็จะโยนหน้าที่มาให้ Activity หรือ Converter ดังนั้นการที่จะมี Event บางอย่างเกิดขึ้น อย่างเช่น ปุ่ม Cancel และ Confirm เจ้าของบล็อกก็จะกำหนดการทำงานแบบนี้


        และก็แน่นอนว่าการลบข้อมูลออกจาก Recycler View ด้วยเช่นกัน แต่ก่อนอื่น...

จะให้ผู้ใช้ลบข้อมูลด้วยวิธีไหนดี?

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

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

ปรับโค้ดของเก่านิดหน่อยเพื่อความพร้อม

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

FakeNetwork.java
...

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

...

เตรียม Resource ที่จำเป็น (ที่ไม่จำเป็น...)

        (สามารถข้ามขั้นตอนนี้ได้เลย)

        เพื่อให้การกดค้างมี Feedback ที่ผู้ใช้รับรู้ได้ ดังนั้นเจ้าของบล็อกขอเพิ่ม Selector ให้กับพื้นหลังของข้อมูลแบบ Order Type เสียหน่อย เวลากดเลือกที่ข้อมูลนั้นๆจะให้เปลี่ยนสีพื้นหลัง จะได้รู้ว่ากดเลือกอยู่

shape_order_background_normal.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="@color/angel_white" />

</shape>

shape_order_background_pressed.xml
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="@color/little_light_gray" />

    <stroke
        android:width="@dimen/selector_stroke_width"
        android:color="@color/angel_white" />

</shape>

shape_order_background_focused.xml
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">

    <solid android:color="@color/angel_white" />

    <stroke
        android:width="@dimen/selector_stroke_width"
        android:color="@color/seasonal_orange" />

</shape>

selector_order_background.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:drawable="@drawable/shape_order_background_pressed" android:state_pressed="true" />

    <item android:drawable="@drawable/shape_order_background_focused" android:state_focused="true" />

    <item android:drawable="@drawable/shape_order_background_normal" />

</selector>


        จากนั้นก็กำหนดให้กับ View ของ Order Type ซะ อย่าลืมกำหนด Focusable ให้เป็น True ด้วย เพราะ Layout โดย Default แล้วจะกำหนด Focusable เป็น False ไว้

view_order.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    ...
    android:background="@drawable/selector_order_background"
    android:focusable="true"
    ...>

    ...

</LinearLayout>

เพิ่มคำสั่งให้ View ของ Order Type เพื่อให้ลบข้อมูลได้

        ถ้ายังจำกันได้ เจ้าของบล็อกกำหนด View ต่างๆที่แสดงใน Recycler View ไว้ใน Adapter ดังนั้นก็ไปเพิ่ม Listener สำหรับ View ของ Order Type ในนั้นซะ

OrderAdapter.java
...

private void setupOrder(OrderViewHolder orderViewHolder, OrderItem orderItem) {
    
    ...

    orderViewHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
        @Override
        public boolean onLongClick(View view) {
            // TODO Do something here
            return true;
        }
    });
}

...

        ถึงจุดนี้ ผู้ที่หลงเข้ามาอ่านบางคนอาจจะเพิ่มคำสั่งให้ลบข้อมูลที่ List<BaseOrderDetailItem> โดยตรงเลย แต่เจ้าของบล็อกไม่ค่อยแนะนำให้ไปลบข้อมูลที่อยู่ใน Adapter ซักเท่าไรนัก เพราะข้อมูลจริงๆนั้นถูกเก็บไว้ใน Activity ดังนั้นเพื่อให้ Flow ถูกต้อง จะต้องส่ง Event ไปบอก Activity เพื่อให้ที่ Activity จะได้ลบข้อมูลที่เลือกทิ้งแล้วอัปเดตข้อมูลใน Adapter ใหม่ซะ

        ตาม Flow ที่แปะไว้ในตอนแรกของบทความนั่นเอง


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

OrderAdapter.java
public class OrderAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    ...

    private void setupOrder(OrderViewHolder orderViewHolder, final OrderItem orderItem) {
        orderViewHolder.tvOrderName.setText(orderItem.getName());
        orderViewHolder.tvOrderDetail.setText(orderItem.getDetail());
        orderViewHolder.tvOrderPrice.setText(orderItem.getPrice());
        orderViewHolder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
            @Override
            public boolean onLongClick(View view) {
                if (onItemClickListener != null) {
                    onItemClickListener.onOrderRemove(orderItem);
                }
                return true;
            }
        });
    }

    ...

    public interface OnItemClickListener {
        void onPositiveButtonClick();

        void onNegativeButtonClick();

        void onOrderRemove(OrderItem orderItem);
    }
}

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

การเปลี่ยนแปลงข้อมูลเกิดขึ้นที่ Activity

        เนื่องจากเจ้าของบล็อกเพิ่ม Event เข้าไปอีกหนึ่งตัว ดังนั้นใน Activity ก็ต้องประกาศเพิ่มด้วยเช่นกัน

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

    ...

    @Override
    public void onOrderRemove(OrderItem orderItem) {
        // TODO Do something
    }
}

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

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

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

    private void setOrderDetail(OrderDetail orderDetail) {
        ...
    }

    @Override
    public void onOrderRemove(OrderItem orderItem) {
        // TODO Do something
    }
}

        จะเห็นว่าข้อมูลที่ได้จากคลาส FakeNetwork ถูกส่งเข้าไปใน setOrderDetail เพื่อกำหนดค่าให้กับ Adapter โดยทันที ซึ่งในความเป็นจริงจะต้องเก็บข้อมูลไว้เป็น Global Variable ด้วย

        แบบนี้ฮะ

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

    private OrderDetail orderDetail;

    ...

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

    ...
}

        เพื่อที่ว่าจะได้เก็บข้อมูลนี้ไว้ที่ Activity และสามารถจัดการ Save/Restore Instance State ได้อย่างถูกต้อง ซึ่งต้องกราบขออภัยผู้ที่หลงเข้ามาอ่านจริงๆที่ไม่ได้ทำแบบนี้ตั้งแต่บทความตอนแรกๆ (

        //เขกโต๊ะ ลงโทษตัวเอง 10 ที

        อ่ะ ทีนี้กลับมาที่ onOrderRemove กันต่อ เมื่อ Order ไหนถูกลบทิ้งก็จะส่งคลาส OrderItem มาด้วย ดังนั้นเจ้าของบล็อกจึงสามารถเอาข้อมูลที่อยู่ในคลาส OrderItem มาเทียบกับคลาส OrderDetail เพื่อลบข้อมูลตัวนั้นๆทิ้งซะ

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

    ...

    @Override
    public void onOrderRemove(OrderItem orderItem) {
        boolean isOrderRemoved = removeOrder(orderDetail, orderItem);
        if (isOrderRemoved) {
            setOrderDetail(orderDetail);
            Toast.makeText(this, "Order " + orderItem.getName() + " was removed", Toast.LENGTH_SHORT).show();
        }
    }

    private boolean removeOrder(OrderDetail orderDetail, OrderItem orderItem) {
        if (orderDetail != null) {
            int index = getFoodOrderIndex(orderDetail.getFoodList(), orderItem);
            if (index != -1) {
                orderDetail.getFoodList().remove(index);
                return true;
            }
            index = getBookOrderIndex(orderDetail.getBookList(), orderItem);
            if (index != -1) {
                orderDetail.getBookList().remove(index);
                return true;
            }
            index = getMusicOrderIndex(orderDetail.getMusicList(), orderItem);
            if (index != -1) {
                orderDetail.getMusicList().remove(index);
                return true;
            }
        }
        return false;
    }

    private int getFoodOrderIndex(List<OrderDetail.Food> foodOrderDetailList, OrderItem orderItem) {
        if (foodOrderDetailList != null && orderItem != null) {
            for (int index = 0; index < foodOrderDetailList.size(); index++) {
                OrderDetail.Food food = foodOrderDetailList.get(index);
                if (food.getOrderName().equals(orderItem.getName())) {
                    return index;
                }
            }
        }
        return -1;
    }

    private int getBookOrderIndex(List<OrderDetail.Book> bookOrderDetailList, OrderItem orderItem) {
        if (bookOrderDetailList != null && orderItem != null) {
            for (int index = 0; index < bookOrderDetailList.size(); index++) {
                OrderDetail.Book book = bookOrderDetailList.get(index);
                if (book.getBookName().equals(orderItem.getName())) {
                    return index;
                }
            }
        }
        return -1;
    }

    private int getMusicOrderIndex(List<OrderDetail.Music> musicOrderDetailList, OrderItem orderItem) {
        if (musicOrderDetailList != null && orderItem != null) {
            for (int index = 0; index < musicOrderDetailList.size(); index++) {
                OrderDetail.Music music = musicOrderDetailList.get(index);
                if (music.getAlbum().equals(orderItem.getName())) {
                    return index;
                }
            }
        }
        return -1;
    }
}

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

        ซึ่งตรงจุดนี้ จริงๆแล้วข้อมูลควรจะมี Unique ID เป็นของตัวเอง แล้วให้ OrderItem เก็บค่า ID ไว้ด้วย เพื่อที่จะได้ใช้ ID ในการเทียบข้อมูลแทน ซึ่งจะเหมาะสมกว่า แต่ในบทความนี้เจ้าของบล็อกไม่ได้ใส่ ID ของสินค้าลงไป จึงใช้วิธีเทียบชื่อสินค้าแทน ต้องขออภัยด้วย...

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

มาทำให้การลบข้อมูลซับซ้อนมากกว่านี้กันเถอะ

        โค้ดที่เจ้าของบล็อกใช้ในตอนนี้จะให้ผลลัพธ์แบบนี้



        ถึงแม้ว่าดูเหมือนจะเสร็จเรียบร้อยแล้ว แต่เอาเข้าจริงถ้าลองสังเกตดีๆจะเห็นว่าตอนที่ลบข้อมูลนั้น สิ่งที่เกิดขึ้นนั้นคือในข้อมูลจะหายไปทันที โดยไม่มี Transition หรือ Animation ใดๆเลย... ซึ่งมันดูไม่ค่อยสวยงามซักเท่าไร

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

        ส่วนหนึ่งที่เจ้าของบล็อกชอบ Recycler View นั่นก็เพราะว่า Adapter ของ Recycler View มี Item Animator ให้ด้วย และโดย Default ก็จะมีการกำหนด Animation เวลาข้อมูลมีการ เพิ่ม/แก้ไข/ย้าย/ลบ ซึ่งจะต้องเรียกใช้คำสั่งเหล่านี้เพื่อให้ข้อมูลอัปเดตแบบมี Animation

        • notifyItemChanged
        • notifyItemMoved
        • notifyItemRemoved
        • notifyItemRangeChanged
        • notifyItemRangeInserted
        • notifyItemRangeRemoved

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

        ฟังดูยุ่งยากกว่าเดิมหรือป่าวนะ...

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

ก็บอกแล้วว่านี่คือการใช้งาน Recycler View กับงานจริงๆที่โคตรซับซ้อน

        ถ้าคิดว่าการใช้คำสั่งพวก notifyItem... ทั้งหลายนั้นเป็นเรื่องที่ยุ่งยาก อย่าลืมว่า Recycler View ของเจ้าของบล็อกก็ไม่ได้แสดงข้อมูลแบบธรรมดาๆเช่นกัน

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


        โคตรบันเทิง....

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

        แต่สำหรับตอนนี้....

ขอแนะนำให้รู้จักกับ DiffUtil ตัวช่วยใหม่ใน Recycler View

        ผู้ที่หลงเข้ามาอ่านหลายๆคนอาจจะเพิ่งเคยได้ยินชื่อคลาสนี้ ซึ่งก็ไม่ใช่เรื่องแปลกอะไร เพราะคลาสตัวนี้เพิ่งถูกเพิ่มเข้ามาใน Recycler View เวอร์ชัน 24.2.0 ซึ่งถือว่ายังไม่นานมากนัก (นับจากช่วงเวลาที่เขียนบทความนี้) ซึ่งคลาส DiffUtil จะมาช่วยให้นักพัฒนาจัดการกับข้อมูลใน Recycler View ที่ซับซ้อนและมีการเปลี่ยนแปลงเมื่อไรก็ได้

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

        ฟังดูเจ๋ง แต่บางอย่างก็ต้องทำเองอยู่นะ แต่ไม่เยอะหรอก

        วิธีการเรียกใช้งาน DiffUtil จะต้องสร้างคลาสขึ้นมาแบบนี้

OrderListDiffCallback.java
import android.support.v7.util.DiffUtil;


public class OrderListDiffCallback extends DiffUtil.Callback {

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

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

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return false;
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        return false;
    }
}

       จากตัวอย่างข้างบนนี้ เจ้าของบล็อกอยากจะใช้ DiffUtil กับข้อมูลของเจ้าของบล็อก จะต้องสร้างคลาสขึ้นมาโดย Extend จาก DiffUtil.Callback แล้วก็จะพบกับ Override Method อยู่ 4 ตัวด้วยกัน

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

OrderListDiffCallback.java
public class OrderListDiffCallback extends DiffUtil.Callback {
    private List<BaseOrderDetailItem> oldOrderItemList;
    private List<BaseOrderDetailItem> newOrderItemList;

    public OrderListDiffCallback(List<BaseOrderDetailItem> oldOrderItemList, List<BaseOrderDetailItem> newOrderItemList) {
        this.oldOrderItemList = oldOrderItemList;
        this.newOrderItemList = newOrderItemList;
    }

    @Override
    public int getOldListSize() {
        return oldOrderItemList != null ? oldOrderItemList.size() : 0;
    }

    @Override
    public int getNewListSize() {
        return newOrderItemList != null ? newOrderItemList.size() : 0;
    }

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        return false;
    }

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        return false;
    }
}

        เพราะข้อมูลที่เจ้าของบล็อกจะโยนให้ Adapter นั้นไม่ใช่คลาส OrderDetail โดยตรง แต่จะแปลงให้กลายเป็น List<BaseOrderDetailItem> ดังนั้นข้างในนี้ก็จะต้องประกาศตัวแปรของ List<BaseOrderDetailItem> จำนวน 2 ตัวด้วยกัน สำหรับข้อมูลชุดเก่าและชุดใหม่ที่มีการเปลี่ยนแปลง และเจ้าของบล็อกได้สร้าง Constructor ที่สามารถโยนข้อมูลทั้ง 2 ตัวเข้ามากำหนดค่าได้เลย ไม่ต้องมี Getter/Setter เพราะไม่ได้ใช้หรอก

        สำหรับคำสั่ง getOldListSize กับ getNewListSize คงเดาได้ไม่ยาก เพราะเอาไว้เช็คจำนวนข้อมูลของแต่ละตัว และจะเหลืออีก 2 คำสั่ง คือ areItemsTheSame กับ areContentsTheSame สำหรับเปรียบเทียบข้อมูลที่อยู่ใน List<BaseOrderDetailItem> แต่ละตัว ซึ่งมีหน้าที่ดังนี้

        • areItemsTheSame เอาไว้เช็คว่าข้อมูลตัวเก่ากับข้อมูลตัวใหม่นั้นคือตัวเดียวกันหรือป่าว โดยควรจะเช็คจาก Unique ID ของข้อมูลตัวนั้นๆ

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

        จากโค้ดตัวอย่างก่อนหน้านี้จะเห็นว่าทั้ง 2 คำสั่งนั้นยังไม่ได้ทำนอกจาก Return ค่าเป็น False

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

ข้อมูลทั้ง 2 ตัวนั้นเป็นข้อมูลตัวเดียวกันหรือป่าว?

        คำสั่ง areItemsTheSame มีไว้เช็คว่าข้อมูลมีการเปลี่ยนแปลงหรือไม่ (สำหรับกรณี Remove, Insert หรือ Move)

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

OrderListDiffCallback.java
public class OrderListDiffCallback extends DiffUtil.Callback {
    private List<BaseOrderDetailItem> oldOrderItemList;
    private List<BaseOrderDetailItem> newOrderItemList;

    ...

    @Override
    public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
        BaseOrderDetailItem newOrderDetailItem = newOrderItemList.get(newItemPosition);
        BaseOrderDetailItem oldOrderDetailItem = oldOrderItemList.get(newItemPosition);
        if (newOrderDetailItem.getType() == oldOrderDetailItem.getType()) {
            if (newOrderDetailItem instanceof SectionItem) {
                SectionItem newSectionItem = (SectionItem) newOrderDetailItem;
                SectionItem oldSectionItem = (SectionItem) oldOrderDetailItem;
                return newSectionItem.getSection().equals(oldSectionItem.getSection());

            } else if (newOrderDetailItem instanceof OrderItem) {
                OrderItem newOrderItem = (OrderItem) newOrderDetailItem;
                OrderItem oldOrderItem = (OrderItem) oldOrderDetailItem;
                return newOrderItem.getName().equals(oldOrderItem.getName());

            } else if (newOrderDetailItem instanceof SummaryItem) {
                SummaryItem newSummaryItem = (SummaryItem) newOrderDetailItem;
                SummaryItem oldSummaryItem = (SummaryItem) oldOrderDetailItem;
                return newSummaryItem.getName().equals(oldSummaryItem.getName());

            } else {
                return true;
            }
        }
        return false;
    }

    ...
}

        เนื่องจากคลาส BaseOrderDetailItem มีการเก็บค่า Type ไว้อยู่แล้ว ก็เลยเอามาเช็คเบื้องต้นก่อนได้เลย และถ้า Type เหมือนกันก็จะทำการ Cast ให้กลายเป็น Type จริงๆของตัวนั้นๆเพื่อดึงชื่อของข้อมูลทั้ง 2 มาเปรียบเทียบกัน

        แต่คลาสที่มีชื่อของข้อมูลอยู่ข้างในจริงๆก็จะมีแค่ SectionItem, OrderItem และ SummaryItem เท่านั้น ดังนั้นในกรณีที่เป็นคลาสใดคลาสหนึ่งในทั้ง 3 คลาสนี้ก็จะมีการ Cast แล้วเทียบชื่อของข้อมูล แต่ถ้าเป็นคลาสอื่นจะให้ผลลัพธ์เป็น True ตลอด และถ้า Type ไม่เหมือนกันตั้งแต่แรก ผลลัพธ์ก็ จะเป็น False ทันที

        คำเตือน - สำหรับคลาส Item ที่ไม่มีข้อมูลอยู่ข้างใน จะเห็นว่าไม่ต้องเขียนอะไรเพิ่ม แต่คลาสเหล่านี้ควรจะมีอยู่ใน Recycler View แค่ประเภทละหนึ่งตัวเท่านั้น (ทางที่ดีทุกตัวควรมี Unique ID)

        ถ้าสังเกตดีๆก็จะเห็นว่า TotalItem จะได้ผลลัพธ์เป็น True เช่นกัน ถึงแม้ว่ามีข้อมูลอยู่ข้างในก็ตาม เพราะเป็น Item ที่แสดงอยู่ตัวเดียวเท่านั้นและไม่มีชื่อของข้อมูล มีแต่ราคารวมสินค้าทั้งหมด

ข้อมูลทั้ง 2 ตัวมีข้อมูลข้างในเหมือนกันหรือป่าว?

        คำสั่ง areContentsTheSame มีไว้เช็คว่าข้อมูลข้างในมีการเปลี่ยนแปลงหรือป่าว (สำหรับกรณี Change)

        สิ่งที่ควรใส่ในนี้ก็คือโค้ดเปรียบเทียบข้อมูลที่มีอยู่ข้างในทั้งหมด เช่น ข้อมูลดังกล่าวเป็นคลาส OrderItem ดังนั้นก็ควรจะเช็คข้อมูลที่มีใน OrderItem ให้ครบทั้งหมด (Name, Detail และ Price)

OrderListDiffCallback.java
public class OrderListDiffCallback extends DiffUtil.Callback {
    private List<BaseOrderDetailItem> oldOrderItemList;
    private List<BaseOrderDetailItem> newOrderItemList;

    ...

    @Override
    public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
        BaseOrderDetailItem newOrderDetailItem = newOrderItemList.get(newItemPosition);
        BaseOrderDetailItem oldOrderDetailItem = oldOrderItemList.get(newItemPosition);
        if (newOrderDetailItem.getType() == oldOrderDetailItem.getType()) {
            if (newOrderDetailItem instanceof SectionItem) {
                SectionItem newSectionItem = (SectionItem) newOrderDetailItem;
                SectionItem oldSectionItem = (SectionItem) oldOrderDetailItem;
                return newSectionItem.getSection().equals(oldSectionItem.getSection()) &&
                        newSectionItem.getBackgroundColor() == oldSectionItem.getBackgroundColor();

            } else if (newOrderDetailItem instanceof OrderItem) {
                OrderItem newOrderItem = (OrderItem) newOrderDetailItem;
                OrderItem oldOrderItem = (OrderItem) oldOrderDetailItem;
                return newOrderItem.getName().equals(oldOrderItem.getName()) &&
                        newOrderItem.getDetail().equals(oldOrderItem.getDetail()) &&
                        newOrderItem.getPrice().equals(oldOrderItem.getPrice());

            } else if (newOrderDetailItem instanceof SummaryItem) {
                SummaryItem newSummaryItem = (SummaryItem) newOrderDetailItem;
                SummaryItem oldSummaryItem = (SummaryItem) oldOrderDetailItem;
                return newSummaryItem.getName().equals(oldSummaryItem.getName()) &&
                        newSummaryItem.getPrice().equals(oldSummaryItem.getPrice());

            } else if (newOrderDetailItem instanceof TotalItem) {
                TotalItem newTotalItem = (TotalItem) newOrderDetailItem;
                TotalItem oldTotalItem = (TotalItem) oldOrderDetailItem;
                return newTotalItem.getTotalPrice().equals(oldTotalItem.getTotalPrice());

            } else {
                return true;
            }
        }
        return false;
    }

    ...
}

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

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

DiffUtil พร้อมแล้ว! เรียกใช้งานกันเถอะ

        เมื่อคลาส OrderListDiffCallback พร้อมใช้งานแล้ว ให้กลับมาดูโค้ดใน MainActivity ก่อน เพราะต้องมีการปรับบางอย่างเพื่อให้ใช้งาน DiffUtil ได้

        ให้สังเหตุที่คำสั่ง callService และ setOrderDetail ที่เดิมทีเขียนไว้แบบนี้

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

    private void callService() {
        FakeNetwork.getFakeOrderDetail(new FakeNetwork.OnResultCallback() {
            @Override
            public void onOrderDetailCallback(OrderDetail orderDetail) {
                MainActivity.this.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);

        int foodTitleColor = ContextCompat.getColor(this, R.color.sky_light_blue);
        int bookTitleColor = ContextCompat.getColor(this, R.color.funny_dark_pink);
        int musicTitleColor = ContextCompat.getColor(this, R.color.natural_green);

        List<BaseOrderDetailItem> orderDetailItemList = new ArrayList<>();
        orderDetailItemList.add(OrderDetailConverter.createUserDetail(name));
        if (isOrderDetailAvailable(orderDetail)) {
            orderDetailItemList.add(OrderDetailConverter.createTitle(yourOrderTitle));
            orderDetailItemList.addAll(OrderDetailConverter.createSectionAndOrder(orderDetail, foodTitle, bookTitle, musicTitle, currency, foodTitleColor, bookTitleColor, musicTitleColor));
            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());
        } else {
            orderDetailItemList.add(OrderDetailConverter.createTitle(yourOrderTitle));
            orderDetailItemList.add(OrderDetailConverter.createNoOrder());
            orderDetailItemList.add(OrderDetailConverter.createTitle(summaryTitle));
            orderDetailItemList.add(OrderDetailConverter.createTotal(orderDetail, currency));
        }
        orderDetailItemList.add(OrderDetailConverter.createEmpty());

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

    ...
}

        ในคำสั่ง setOrderDetail นอกจากจะแปลงข้อมูลให้กลายเป็น List<BaseOrderDetailItem> แล้ว ยังมีคำสั่งกำหนดข้อมูลที่ได้ลงใน Adapter แล้วสั่ง notifyDataSetChanged จากในนี้ทันที

        ซึ่งเจ้าของบล็อกขอแยกคำสั่งในส่วนนี้ออกจากกัน โดยเปลี่ยนคำสั่ง setOrderDetail ให้กลายเป็น createOrderDetailList แล้วคำสั่งที่เกี่ยวกับ Adapter จะแยกออกมาเป็นอีกคำสั่ง โดยกำหนดชื่อคำสั่งนั้นว่า updateOrderDetailList

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

    private void callService() {
        FakeNetwork.getFakeOrderDetail(new FakeNetwork.OnResultCallback() {
            @Override
            public void onOrderDetailCallback(OrderDetail orderDetail) {
                MainActivity.this.orderDetail = orderDetail;
                List<BaseOrderDetailItem> orderDetailItemList = createOrderDetailList(orderDetail);
                List<BaseOrderDetailItem> emptyList = new ArrayList<>();
                updateOrderDetailList(emptyList, orderDetailItemList);
            }
        });
    }

    private List<BaseOrderDetailItem> createOrderDetailList(OrderDetail orderDetail) {

        ...

        List<BaseOrderDetailItem> orderDetailItemList = new ArrayList<>();
        orderDetailItemList.add(OrderDetailConverter.createUserDetail(name));
        if (isOrderDetailAvailable(orderDetail)) {
            orderDetailItemList.add(OrderDetailConverter.createTitle(yourOrderTitle));
            orderDetailItemList.addAll(OrderDetailConverter.createSectionAndOrder(orderDetail, foodTitle, bookTitle, musicTitle, currency, foodTitleColor, bookTitleColor, musicTitleColor));
            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());
        } else {
            orderDetailItemList.add(OrderDetailConverter.createTitle(yourOrderTitle));
            orderDetailItemList.add(OrderDetailConverter.createNoOrder());
            orderDetailItemList.add(OrderDetailConverter.createTitle(summaryTitle));
            orderDetailItemList.add(OrderDetailConverter.createTotal(orderDetail, currency));
        }
        orderDetailItemList.add(OrderDetailConverter.createEmpty());
        return orderDetailItemList;
    }

    private void updateOrderDetailList(List<BaseOrderDetailItem> oldOrderItemList, List<BaseOrderDetailItem> newOrderItemList) {
        orderAdapter.setOrderItemList(newOrderItemList);
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new OrderListDiffCallback(oldOrderItemList, newOrderItemList));
        diffResult.dispatchUpdatesTo(orderAdapter);
    }

    @Override
    public void onOrderRemove(OrderItem orderItem, int position) {
        List<BaseOrderDetailItem> oldOrderItemList = createOrderDetailList(orderDetail);
        boolean isOrderRemoved = removeOrder(orderDetail, orderItem);
        List<BaseOrderDetailItem> newOrderItemList = createOrderDetailList(orderDetail);

        if (isOrderRemoved) {
            updateOrderDetailList(oldOrderItemList, newOrderItemList);
            Toast.makeText(this, "Order " + orderItem.getName() + " was removed", Toast.LENGTH_SHORT).show();
        }
    }

    ...
    
}

        ให้สังเกตที่คำสั่ง updateOrderDetailList เพราะการเรียกใช้งานคลาส DiffUtil จะอยู่ในนั้น โดยคำสั่งดังกล่าวจะมีรูปแบบดังนี้

MainActivity.java
...

private void updateOrderDetailList(List<BaseOrderDetailItem> oldOrderItemList, List<BaseOrderDetailItem> newOrderItemList) {
    orderAdapter.setOrderItemList(newOrderItemList);
    DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new OrderListDiffCallback(oldOrderItemList, newOrderItemList));
    diffResult.dispatchUpdatesTo(orderAdapter);
}

...

        ในคำสั่ง DiffUtil.calculateDiff นั้นเจ้าของบล็อกจะสร้าง OrderListDiffCallback ขึ้นมาพร้อมกับกำหนดค่าข้อมูลชุดเก่ากับชุดใหม่เข้าไปเพื่อให้คลาสดังกล่าวเปรียบเทียบแล้วส่งผลลัพธ์ออกมาเป็นคลาส DiffUtil.DiffResult ซึ่งคลาสดังกล่าวนี้จะมีคำสั่งเพื่อบอกผลลัพธ์ว่ามีอะไรเปลี่ยนแปลงบ้าง

void dispatchUpdatesTo(ListUpdateCallback callback)
void dispatchUpdatesTo(RecyclerView.Adapter adapter)


        ถ้าอยากรู้ว่ามีอะไรเปลี่ยนแปลงบ้างก็ให้ใช้ ListUpdateCallback

DiffUtil.DiffResult diffResult = ...
diffResult.dispatchUpdatesTo(new ListUpdateCallback() {
    @Override
    public void onInserted(int position, int count) {
        // Do something
    }

    @Override
    public void onRemoved(int position, int count) {
        // Do something 
    }

    @Override
    public void onMoved(int fromPosition, int toPosition) {
        // Do something
    }

    @Override
    public void onChanged(int position, int count, Object payload) {
        // Do something
    }
});


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

DiffUtil.DiffResult diffResult = ...
diffResult.dispatchUpdatesTo(orderAdapter);

มาดูผลลัพธ์ที่ได้จากการใช้ DiffUtil กันเถอะ

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

        เมื่อคำสั่งทั้งหมดพร้อมทำงานแล้ว ผลลัพธ์ที่เกิดขึ้นก็จะได้ออกมาเป็นแบบนี้


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

        แต่ถ้าอยากจะเปลี่ยนรูปแบบของ Animation ก็สามารถกำหนด Item Animator ของ Adapter ตัวนั้นๆได้ทันที ไม่มีผลอะไรกับ DiffUtil เพราะ DiffUtil ทำหน้าที่เช็คการเปลี่ยนแปลงของข้อมูลแล้วสั่งให้เกิด Animation เท่านั้น ส่วนรูปแบบ Animation ที่เกิดขึ้นก็เป็นหน้าที่ของ Item Animator อยู่เหมือนเดิม

ถ้าอยากจะเพิ่มหรือแก้ไขข้อมูลจะต้องทำยังไง?

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

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

สรุป

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

        เอาล่ะ บทความตอนต่อไปจะเป็นยังไง ก็ต้องอดใจรอก่อนนะจ๊ะ XD (ไม่คิดว่าจะเขียนออกมาได้หลายตอนแบบนี้)