11 September 2017

เล่าสู่กันฟังกับ Architecture Components จากงาน GDD Europe 2017

Updated on
        ไม่กี่วันที่ผ่านมาเจ้าของบล็อกได้มีโอกาสไปงาน Google Developer Days Europe 2017 ที่ภายในงานได้มี Session เกี่ยวกับ Architecture Components ที่มีเนื้อหาน่าสนใจอยู่ไม่น้อย จึงหยิบมาเขียนเป็นบทความเพื่อเล่าสู่กันฟังครับ

ก่อนจะเริ่ม

        บทความนี้จะถือว่าผู้ที่หลงเข้ามาอ่านรู้จักกับ Architecture Components มาก่อนแล้ว ดังนั้นจะไม่มีการเกริ่นเนื้อหาเบื้องต้นนะครับ

        Session ดังกล่าวมีชื่อว่า Architecture Components พูดโดย Florina Muntenescu ถ้าอยากดูเนื้อหาแบบละเอียด ก็กดดูจากวีดีโอข้างล่างนี้ได้เลย


        แต่ถ้าขี้เกียจดูวีดีโอ ก็อ่านต่อได้เลย

ทบทวน View Model กับ Activity/Fragment Lifecycle

        View Model นั้นเป็นอะไรที่เจ้าของบล็อกรู้สึกว่าขี้โกงมากๆ เพราะว่ามันสามารถทำงานร่วมกับ Activity/Fragment Lifecycle ได้อย่างมหัศจรรย์ราวกับพระเจ้าเสกมาให้เป็นเนื้อคู่กันเลยก็ว่าได้ โดยที่เราไม่ต้องไปทำอะไรกับมันมากนัก นอกจากการทำความเข้าใจความสัมพันธ์ของเนื้อคู่ชุดนี้


        • อยู่รอดทุกครั้งเมื่อเกิด Configuration Changes
        • ถูกทำลายเมื่อผู้ใช้กดปุ่ม Back เพื่อออกจาก Activity/Fragment ที่คู่กับ View Model นั้นๆ
        • ถูกทำลายเมื่อผู้ใช้ปิดแอปจาก Recent Apps
        • ถูกทำลายเมื่อแอปถูกย่อและถูกปิด Process ของแอปเพื่อคืน Memory ให้กับระบบ

Avoid reference to Views in ViewModels

        ถ้าไม่จำเป็นก็อย่าเอา UI Logic ไว้ใน View เกินจำเป็น พยายามเอาส่วนที่เป็น Logic ไปไว้ใน View Model แทน ซึ่งจะช่วยให้สามารถเขียน Unit Test ได้ง่ายขึ้น

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


        ส่วน Repository จะคอยทำหน้าที่ดึงข้อมูลมาให้ View Model และจะดึงข้อมูลจาก API หรือว่า Database นั้นก็ขึ้นอยู่กับ Repository ส่วน View Model ไม่ต้องมายุ่งเรื่องนี้เลย รอรับข้อมูลอย่างเดียวก็พอ


Add a data repository as the single-point entry to your data

        ควรแยก Data Layer กับ Presentation Layer ให้เป็นอิสระต่อจากกัน เพราะว่าการทำงานใน Repository นั้นค่อนข้างจะซับซ้อน ไม่ว่าจะเป็นการจัดการกับข้อมูลใน Database, จัดการกับ Cache หรือการทำงานอื่นๆที่เป็น Synchronous

        และเพื่อไม่ให้มีการเรียกข้อมูลจาก API บ่อยจนเกินจำเป็น ควรเก็บข้อมูลไว้ใน Database ด้วยทุกครั้งเพื่อที่จะได้ไม่ต้องไปดึงข้อมูลจาก API ทุกครั้งเมื่อ Activity/Fragment เกิดการ Restore State


        ซึ่งในส่วนของ Database ก็สามารถใช้ Room ที่เป็น Object Mapping Library เพื่อช่วยให้จัดการกับ Database ได้ง่ายขึ้น เพราะว่า Room นั้นสามารถทำงานร่วมกับ LiveData ได้ เวลาที่ข้อมูลใน Database มีการเปลี่ยนแปลงเกิดขึ้น Live Data ก็จะแจ้งให้ทันทีเพื่ออัพเดทข้อมูลใหม่

Guide to App Architecture

How many LiveData object should I expose? 

        ถ้าใน Activity/Fragment นั้นๆมีการแสดงข้อมูลหลายๆอย่าง การแบ่ง View Model ตามข้อมูลแต่ละส่วนก็จะช่วยให้จัดการได้ง่ายกว่า รวมไปถึงการเขียนเทสด้วย


What if I'm using MVP? Should I switch to MVVM?

        ขึ้นอยู่กับว่า Logic ที่มีและเขียนเทสครอบคลุมไว้มากน้อยแค่ไหน ซึ่งตาม Concept ก็ควรจะเอา Logic ไว้ใน Activity/Fragment ให้น้อยที่สุดเท่าที่จะเป็นไปได้

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


        แต่เนื่องจาก Presenter ถูกออกแบบมาให้เลี่ยงการเรียกใช้คลาสของ Android Framework จึงทำให้ View Model พลอยเรียกใช้ไม่ได้ไปด้วย

Should I use LiveData if I'm already using RxJava?

        ถ้าโปรเจคไหนมีใช้ RxJava อยู่แล้ว ก็จะมีลักษณะแบบนี้


        RxJava จะถูกใช้งานในทุกๆส่วน ไม่ว่าจะเป็นการทำงานในส่วนที่ติดต่อกับ API, Database และ Repository ก็จะถูกส่งข้อมูลกลับมาในรูปแบบของ Flowable หรือ Observable และ View Model ก็จะเป็นแบบนี้เหมือนกัน

        แต่การใช้ RxJava ควบคู่กับ Live Data ก็น่าสนใจกว่า โดยแบ่งการทำงานออกเป็น 2 ส่วน ระหว่าง Live Data กับ RxJava โดยใช้ Live Data กับการทำงานในส่วนของ UI เพราะว่า Live Data จะช่วยจัดการกับ Lifecycle ได้ง่ายกว่า ในขณะที่ RxJava ต้องจัดการอะไรหลายๆอย่าง


        ถ้ามีการเรียกใช้ Disposable หรือ Subscription ของ RxJava ก็จะอยู่ใน View Model แทน และเมื่อ View Model ถูกทำลายลง ก็ให้เคลียร์ Disposable หรือ Subscription จากใน View Model ได้เลย

Saving Data

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

        ต้องเก็บไว้ใน Database ตอนไหน?

        และจะต้องเก็บไว้ใน onSavedInstanceState ตอนไหน?

        ก่อนจะพูดถึงเรื่องข้อมูลเหล่านั้น มาดูเหตุการณ์ทั้ง 3 แบบที่สามารถเกิดขึ้นกับ View Model กันก่อน

แบบที่หนึ่ง : Configuration Changes

        View Model จะยังคงมีชีวิตอยู่อย่างมีความสุข หลังจากเกิด Configuration Changes แล้วกลับมาทำงานต่ออีกครั้ง เมื่อ View Model ที่ถูก Observe ใหม่อีกครั้งก็จะส่งข้อมูลเดิมกลับมาให้ทันที


        ดังนั้นในกรณีนี้ View Model จะทำงานจบในตัวเอง ไม่มีการเรียกใช้งานอะไรใน Repository เลย

แบบที่สอง : ย่อแอป แล้วเปิดขึ้นมาใช้งานต่อจากเดิม

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


แบบที่สาม : ย่อแอป แล้วถูก Kill Process ทิ้งเพื่อคืน Memory ให้กับระบบ

        onSavedInstanceState จะทำงาน และ View Model จะถูกทำลายทิ้ง ซึ่งผู้ที่หลงเข้ามาอ่านจะต้องเก็บข้อมูลบางส่วนไว้ใน onSavedInstanceState (ยกตัวอย่างเช่น Unique ID ของข้อมูลนั้นๆ)


        เมื่อแอปกลับมาทำงานใหม่อีกครั้งก็จะมีการ Restore ข้อมูลดังกล่าวขึ้นมา (ยกตัวอย่างเช่น Unique ID ของข้อมูลนั้นๆ) เพื่อส่งให้ View Model และส่งต่อให้ Repository เพื่อไป Query ข้อมูลเต็มๆจาก Database อีกทีหนึ่ง (เพราะตอนที่เรียกข้อมูลจาก API จะเก็บลงไว้ใน Database ด้วยทุกครั้ง ทำให้ไม่ต้องไปเรียกข้อมูลจาก API ใหม่)

จัดการกับข้อมูลให้เหมาะสม

        เนื่องจาก Database ทำหน้าที่เก็บข้อมูลทั้งหมดไว้ให้แล้ว และ View Model ก็จะเก็บข้อมูลแค่ส่วนที่ต้องใช้ใน UI ดังนั้นจึงไม่จำเป็นต้องเก็บข้อมูลก้อนนั้นทั้งหมดไว้ใน onSavedInstanceState ให้ซ้ำซ้อน แต่ให้เก็บข้อมูลบางส่วนที่ใช้เป็น Reference ในการ Query ข้อมูลใหม่อีกครั้งจาก Database ก็พอ

        • Database : เก็บข้อมูลที่อยากให้อยู่ยงคงกระพัน ถึงแม้ว่าจะโดน Kill Process ก็ตาม (มักจะเป็นข้อมูลดิบๆที่ได้จาก API)

        • View Model : เก็บข้อมูลเฉพาะส่วนที่จำเป็นสำหรับ UI

        • onSavedInstanceState : เก็บข้อมูลส่วนที่เล็กที่สุดที่สามารถนำไป Query จาก Database ทีหลังได้ เพื่อใช้ในตอน Restore UI (Restauration Data) จะได้ไม่ต้องไปเรียกข้อมูลจาก API ใหม่อีกครั้ง

Paging Library

        ถึงแม้ว่า View Model, Live Data, Room หรือ Lifecycle Owner จะเข้ามาช่วยให้ควบคุมการทำงานได้ง่ายมากขึ้น แต่แอปพลิเคชันส่วนใหญ่มักจะต้องแสดงข้อมูลจำนวนมากๆด้วย Recycler View ที่มีการทำงานหลายๆอย่างที่ต้องเขียนเพิ่ม (อย่างการทำ Load More) กลายเป็นว่าการนำ Architecture Components เข้ามาใช้ ก็จะต้องมานั่งคิดรูปแบบการทำงานของโค้ดใหม่อยู่ดี เพื่อให้ Recycler View สามารถทำงานแบบที่ควรจะเป็นได้

        ดังนั้นทางทีมพัฒนาจึงได้เพิ่ม Component ตัวที่ 5 เข้ามาใน Architecture Components นั่นก็คือ Paging เพื่อใช้งานกับ Recycler View โดยเฉพาะ

        โดย Paging จะเข้ามาช่วยจัดการกับการทำงานของ Recycler View ที่จะต้องคอยโหลดข้อมูลเพิ่มทุกครั้งที่ผู้ใช้เลื่อนดูข้อมูล (Load More) ที่สามารถกำหนดรูปแบบในการแสดงข้อมูลได้เองไม่ว่าจะเป็น Infinite Scrolling หรือว่า Countable Scrolling โดยที่ไม่ต้องไปเขียนโค้ดอะไรเองมากนัก


        Paging จะมีคลาสหลักที่ชื่อว่า PagedListAdapter ซึ่งเป็นคลาสที่สืบทอดมาจาก RecyclerView.Adapter น่ะแหละ พร้อมกับคลาสอีก 2 ตัวคือ PagedList และ DataSource

DataSource : Handles incremental data loading

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

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

        • Keyed DataSource : DataSource ที่ต้องใช้ข้อมูลลำดับที่ N-1 เพื่อดึงข้อมูลที่ลำดับ N

        • Tiled DataSource : DataSource ที่สามารถกำหนดตำแหน่งของข้อมูลที่ต้องการได้ทันที

        โดย DataSource จะมี Implement Method ดังนี้

int loadCount()

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

PagedList

        ทำหน้าที่รับข้อมูลต่อจาก DataSource โดยอัตโนมัติผ่าน Background Thread แล้วส่งข้อมูลนั้นๆออกมาเป็น Main Thread (เหยดดดด จัดการให้ด้วย)

        ซึ่งรูปแบบในการแสดงข้อมูลก็จะกำหนดผ่าน PagedList ทั้งหมด ไม่ว่าจะเป็น

        • แสดงผลเป็นแบบ Infinite Scrolling List หรือ Countable List

        • จำนวนข้อมูลที่จะโหลดมาแสดงในตอนเริ่มต้น (Initial load size)

        • จำนวนข้อมูลที่จะโหลดมาแสดงเพิ่มในแต่ละครั้ง (Page size)

        • ระยะห่างของลำดับข้อมูลตัวสุดท้ายที่จะทำการ Prefetch ข้อมูลไว้ให้ล่วงหน้า (Prefetch distance)

ลำดับการทำงานของ Paging


        1. สมมติว่ามีข้อมูลอยู่ก้อนหนึ่ง และแล้วส่งข้อมูลเข้าไปใน DataSource บน Background Thread

        2. DataSource จะคอยอัพเดทข้อมูลไปให้ PagedList (ผ่าน Background Thread เช่นกัน)

        3. PagedList กำหนดรูปแบบการแสดงข้อมูล แล้วส่งข้อมูลตามรูปแบบนั้นๆ มาเป็น Main Thread

        4. PagedList ส่งข้อมูลให้ PagedListAdapter ผ่าน Main Thread

        5. PagedListAdapter คำนวณว่าข้อมูลต่างจากข้อมูลตัวเดิมอย่างไร โดยใช้ DiffUtil (ทำงานบน Background Thread)

        6. ข้อมูลถูกส่งมาทาง onBindViewHolder ที่ทำงานบน Main Thread

        7. Recycler View แสดงผลตามข้อมูลที่เปลี่ยนแปลงไป

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

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

DiffCallback<User> DIFF_CALLBACK = new DiffCallback<User>() {
    @Override
    public boolean areContentsTheSame(@NonNull User oldItem, @NonNull User newItem) {
        return oldItem.equals(newItem);
    } 

    @Override
    public boolean areItemTheSame(@NonNull User oldItem, @NonNull User newItem) {
        return oldItem.getId() == newItem.getId();
    }
} 

        DiffCallback จะมี Implement Method ด้วยกัน 2 ตัวคือ

         • areContentsTheSame : เทียบว่าข้อมูลทั้ง 2 ตัวมีข้อมูลที่เหมือนกันหรือไม่

        • areItemTheSame : เทียบว่าข้อมูลทั้ง 2 ตัวคือตัวเดียวกันหรือไม่

        ซึ่งการเทียบข้อมูลทั้ง 2 ตัวนั้นขึ้นอยู่กับการออกแบบ Model Class ของผู้ที่หลงเข้ามาอ่าน (ในตัวอย่างจะสมมติว่าเป็นคลาสที่สร้างขึ้นมาเองที่ชื่อว่า User ที่ข้อมูลแต่ละตัวจะมี ID ที่ไม่เหมือนกัน)

        นอกจากนี้ PagedList นั้น สามารถใช้คลาส LivePagedListProvider แทนได้ ซึ่งเป็น PagedList ที่ครอบด้วย LiveData อีกชั้นหนึ่งเพื่อให้ทำงานร่วมกับ DataSource ที่เป็น LiveData ได้

        และนั่นก็หมายความว่ามันสามารถใช้กับ Room ได้ด้วย และถ้าใช้ LivePagedListProvider ควบคู่กับ Room ก็จะสามารถจัดการในส่วนของ DataSource โดยที่ไม่ต้องทำอะไรเพิ่มเติมเลย เพราะว่า Room จะคอยอัปเดตข้อมูลให้ทันทีเมื่อมีการเปลี่ยนแปลง แล้วส่งให้ LivePagedListProviedr ทันที (แล้ว RecyclerView ก็จะแสดงผลตาม Database แทบจะ Realtime กันเลยทีเดียว)

        ดังนั้น DataSource ที่เป็น Room เราจึงสามารถใช้ LivePagedListProvider แบบนี้ได้เลย

@Dao
interface UserDao {
    @Query("SELECT * FROM user ORDER BY lastName ASC")
    public abstract LivePagedListProvider<User> userByLastName();
}

        เมื่อมาดูคำสั่งใน View Model

class MyViewModel extends ViewModel {
    public final LiveData<PagedList<User>> userList;

    public MyViewModel(UserDao userDao) {
        userList = userDao.userByLastName()
                .create(new PagedList.Config.Builder()
                .setPageSize(50)
                .setPrefetchDistance(50)
                .build());
    }
}

        จะเห็นว่าสามารถกำหนดรูปแบบในการดึงข้อมูลจาก Database ได้เลย เพราะ UserDao ส่งข้อมูลกลับมาเป็น LivePagedListProvider นั่นเอง

        ส่วนฝั่ง Activity/Fragment ก็สามารถดึงข้อมูลจาก View Model มากำหนดใน RecyclerView แบบนี้ได้เลย

class MyActivity extends Activity implements LifecycleRegistryOwner {
    @Override
    public void onCreate(Bundle savedState) {
        MyViewModel viewModel = ViewModelProviders.of(this).get(MyViewModel.class);
        RecyclerView recyclerView = findViewById(R.id.user_list);
        UserAdapter<User> adapter = new UserAdapter();
        LiveListAdapterUtil.bind(viewModel.userList, this, adapter);
        recyclerView.setAdapter(adapter);
    } 
} 

        LiveListAdapterUtil จะทำหน้านี้จัดการให้ข้อมูลจาก ViewModel, Adapter และ Lifecycle ทำงานร่วมกันให้โดยอัตโนมัติ ที่เหลือก็เป็นคำสั่งปกติที่รู้ๆกัน

        ส่วน Adapter ที่ใช้เป็นคลาส PagedListAdapter ก็จะเรียกใช้งานเหมือน RecyclerView Adapter ทั่วไป เพิ่มเติมแค่กำหนด DiffCallback ไว้ใน Constructor ด้วยเท่านั้นเอง

class UserAdapter extends PagedListAdapter<User, UserViewModel> {
    public UserAdapter() {
        super(User.DIFF_CALLBACK);
    } 

    @Override
    public void onBindViewHolder(UserViewHolder holder, int position) {
        User user = getItem(position);
        if (user != null) {
            holder.bindTo(user);
        } else {
            // Null defines a placeholder item
            holder.clear();
        }
    } 
} 

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

        แต่ถ้าจะให้สรุปถึงความสามารถของ Paging ล่ะก็

        Paging ทำให้ผู้ที่หลงเข้ามาอ่านสามารถสร้าง Recycler View ที่ต้องทำเป็น Pagination ได้ง่ายขึ้น

สรุป

        ถือว่าเป็น Session ที่น่าจะตอบคำถามคาใจนักพัฒนาหลายๆคนที่อยากจะรู้แนวทางในการนำ Architecture Components อย่างถูกต้อง รวมไปถึง Component ตัวใหม่อย่าง Paging ที่ช่วยให้ชีวิตง่ายขึ้นไปอีก ที่เหลือก็ต้องดูต่อว่าถ้าจะเอาไปใช้กับโปรเจคของผู้ที่หลงเข้ามาอ่านจะต้องทำอะไรเพิ่มเติมบ้าง ส่งผลกระทบอะไรมั้ย และอย่าลืมเขียนเทสล่ะ ฮาาาา


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