Android Architecture Components คืออะไร แบบม้วนเดียวจบ

Minseo Chayabanjonglerd
Fungjai
Published in
6 min readSep 25, 2017

Android Developer หลายๆคนเริ่มพูดถึงกันแล้วในประเด็นนี้ กับ Architecture Components อยู่วงการนี้ต้องไวนะ ไม่ว่า Android จะปล่อยอะไรให้เราได้เล่นบ้าง เราต้องตามให้ทันนะ เออแล้วมันคืออะไรหล่ะ

A new collection of libraries that help you design robust, testable, and maintainable apps. Start with classes for managing your UI component lifecycle and handling data persistence.

Architecture Components เป็นตัวช่วยในการจัดการ structure ของ android app เราให้ดีขึ้น สามารถแยกส่วนต่างๆเป็น atomic ได้มากขึ้น ดังนั้นจึงนำไปทดสอบได้ง่ายขึ้นและเร็วขึ้น และสามารถจัดการได้ง่ายขึ้น เนื่องจาก structure จะซับซ้อนน้อยลง ในทีมสามารถอ่านและเขียน feature ใหม่อย่างเป็นระเบียบมากขึ้น ที่เขาเน้นหลักๆ เช่น

รายละเอียดเพิ่มเติมตามลิ้งข้างล่างนี้ ซึ่งเขามี codelab ให้เราลองทำเพื่อเสริมสร้างความเข้าใจด้วย

การเพิ่ม component ในโปรเจก ไปเพิ่มคำสั่งใน build.gradle ของโปรเจก แบบนี้

allprojects {
repositories {
jcenter()
maven { url 'https://maven.google.com' }
}
}

ต่อมาเปิด build.gradle ของแอปเรา เพื่อเพิ่ม repository ปัจจุบันเป็น version 1.0.0 Alpha 9–1 (เมื่อ 13 กันยายน 2560)ใน dependencies ดังนี้

dependencies {
...
// for Lifecycles, LiveData, and ViewModel
implementation "android.arch.lifecycle:runtime:1.0.0"
implementation "android.arch.lifecycle:extensions:1.0.0-alpha9-1"
annotationProcessor "android.arch.lifecycle:compiler:1.0.0-alpha9-1"
// for Room
implementation "android.arch.persistence.room:runtime:1.0.0-alpha9-1"
annotationProcessor "android.arch.persistence.room:compiler:1.0.0-alpha9-1"
// for testing Room migrations
testImplementation "android.arch.persistence.room:testing:1.0.0-alpha9-1"
// for Room RxJava support
implementation "android.arch.persistence.room:rxjava2:1.0.0-alpha9-1"
// for Paging
implementation "android.arch.paging:runtime:1.0.0-alpha1"
}

มีตัวอย่างให้เราได้ clone ไปเล่นใน github โดยเริ่มที่ basic sample ก่อนนะ

ก่อนที่จะลงรายละเอียดลึกกว่านี้ มาดูวิดีโอแนะนำ Architecture Components กันดีกว่า

พี่สาวท่านนี้บรีฟให้สั้นๆ

ใน Google I/O 2017 ก็มีพูดถึงเหมือนกัน เอ๊ะ สองวิดีโอนี้อัพวันเดียวกันเลย คือ วันที่ 15 พฤษภาคม แต่ส่วนใหญ่ไปกรี๊ดกร๊าดกับ Kotlin กัน

จริงๆมีหลายคลิปในงานเดียวกัน จากการประมาณด้วยสายตา รวมความยาวน่าจะ 2–3 ชั่วโมงได้ ค่อนข้างยาวนานและน่าเบื่อทีเดียวสำหรับคนที่ภาษายังงงๆเหมือนเรา

ใน GDD Europe 2017 ก็มีพูดถึงเช่นกัน

อันนี้ละเอียดมากๆ ฟังง่ายด้วย ยากอย่างเดียวคือชื่อพี่เขานี่แหละ

และพี่เอกก็ได้อธิบายแบบ shot-by-shot ให้ฟัง แบบนี้

คร่าวๆจะเป็นโครงสร้างแบบ MVVM คือ Model-View-View Model ซึ่งเราเคยเขียนมาแล้ว ใน C# นะ และใน android ก็ทำได้ โดยการใช้ data binding นั่นแหละ

ในตอนนี้เราก็ได้ลองทำ codelab ดู ซึ่งตัวนี้รองรับ Android Studio 2.3 ขึ้นไป และใช้ Support Library 26.1.0

มาสรุปสิ่งที่ได้จากการศึกษาข้อมูลขั้นต้น และจากการทำ codelab ซึ่งมี codelab ให้ลองทำสองตัว คือ Lifecycle และ Room มาเริ่มจากตัวแรกดีกว่า

Lifecycle

ในตัวนี้จะมีรูปภาพที่เป็น main concept ว่า ViewModel มีความสัมพันธ์อย่างไร กับ Activity/Fragment ดังนี้

กรอบสีเทา คือ event ที่เกิดขึ้น คือ เปิดแอป หมุนจอ กดปิด, กรอบสีส้มและสีฟ้า คือ lifecycle แบบปกติที่เราคุ้นเคยกัน อย่างการหมุนจอเนี่ย มัน onDestroy ของเดิมและ onCreate สร้างมันขึ้นมาใหม่, ส่วนกรอบสีเขียวนั่นคือ ViewModel Scope ซึ่งจะมี onCleared() เคลียร์ของออกจากแอปก่อนที่แอปจะตายอย่างสมบูรณ์; credit : https://developer.android.com/topic/libraries/architecture/viewmodel.html

อันนี้แก้ปัญหาเรื่อง Configuration change เช่น หมุนจอ มันทำให้ orientation และ screen size เปลี่ยนไป อีกอันก็คือเปลี่ยนภาษา บางแอปก็มีการ set string ในแอปทั้งภาษาไทยและภาษาอังกฤษ ซึ่งเขาจะอธิบายเรื่องหมุนจอนี่แหละ พอเราหมุนจอแล้ว lifecycle มันจะไป onDestroy() แล้ว onCreate() ใหม่ เท่ากับว่าโหลด Activity/Fragment ใหม่หมดเลย พอเอา ViewModel มาช่วย ทำให้สามารถทำงานได้อย่างต่อเนื่อง แม้หมุนจอก็ตาม

แต่มันจะตายเมื่อเรากด back กลับ, ปิดแอปจาก recent app, และ โดน kill โดยการเคลียร์ RAM ในเครื่อง เป็นการคืนพื้นที่ memory ภายในเครื่องเรานั่นเอง

ใน codelab จะสร้าง class ViewModel ขึ้นมาใหม่โดย extends ViewModel มา
จากนั้นเอามาใช้ใน Activity/Fragment ของเรา แบบนี้

ChronometerViewModel chronometerViewModel
= ViewModelProviders.of(this).get(ChronometerViewModel.class);

ข้อควรระวัง การยัดค่าไปใส่ใน Context หรือ View จะทำให้ memory leak ได้นะยูวว ดังนั้นเราต้องเคลียร์ค่าออก โดยการใส่ไว้ใน onCleared() ….ก็ไม่ช่วยอะไร จริงๆคืออันนี้เคลียร์ได้เกือบหมด ยกเว้น Context กับ View นั่นแหละ เขาแนะนำว่าอย่ายัดใส่สองตัวนี้เลย

อันที่ผิดเป็นประมาณไหนอ่ะ?

ใน web google developer ไม่มีตัวอย่างที่ผิด เพียงแต่บอกมาลอยๆ เลยถามอากู๋ เลยเจอตัวอย่างมาอันนึง เหมือนเขา pass View ไป ViewModel

ref: https://github.com/googlesamples/android-architecture-components/issues/41

เราใช้ ViewModel ในการ config ตัว Observe พวก data source เราสามารถใช้กับ data binding และ RxJava ได้ด้วย
ส่วน View ก็อยู่เฉยๆ คอย Observe แล้วก็นำข้อมูลจากใน ViewModel มาแสดง

LiveData ทำหน้าที่ observe การเปลี่ยนแปลงของข้อมูลระหว่างหลายๆ components และส่ง update ไปยัง Activity หรือ LifecycleOwner active

LifecycleRegistryOwner ถูก bind โดยค่าของ lifecycle ของ ViewModel และ LiveData ของ Activity/Fragment นั้นๆ

การ Subscribe ของแต่ละ lifecycle event ปกติเวลาที่เราเขียน Activity/Fragment เราก็จะ override class กันแบบนี้

@Override
protected void onResume() {
mLocationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 0, 0, mListener);
}

@Override
protected void onPause() {
mLocationManager.removeUpdates(mListener);
}

เราใช้ LifecycleObserver ในการ Subscribe โดยเริ่มจาก implement ตัว LifecycleObserver เข้ามา แล้วใส่อันนี้ใน constructer ของ class ที่เราทำ

lifecycleOwner.getLifecycle().addObserver(this);

จากนั้นเราก็ Subscribe กันแบบนี้ ซึ่งใช้แทน override แบบเดิมนั่นแหละ

@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
void addLocationListener() {
...
}

@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
void removeUpdatesListener() {
...
}

นอกจากในตัวอย่างแล้ว เราสามารถใช้ @OnLifecycleEventและมีบาง event ที่รองรับ ดังรูป
(ON_ANY อันนี้คือ all events เลยแหะ)

และเราสามารถแชร์ ViewModel กับ fragment หลายๆอันได้ด้วยนะ โดยใช้ observe กับ LifecycleOwner เข้ามาช่วยด้วย ตัวอย่างจากใน codelab step 5 ของ lifecycler เน้อ

แปะใน medium มันไม่สวยอ่ะ เลยแปะใน gist แทน หน้านี้จะมี seekbar สองตัว บนล่าง ตอนแรกตัว seekbar จะแยกกันเป็นอิสระ พอใช้ observe ทำให้ดึง seekbar จากบนหรือล่าง จะไปด้วยกันทั้งคู่

Room

เขายกตัวอย่างการใช้กับ SQLite นะ ซึ่งดูง่ายมากๆ ท่าคล้ายๆกับการเรียก API เลยทีเดียว

ทำไมต้องใช้กับ SQLite? เพราะว่า concept เขาคือการเก็บ data หรือ cache ใน local พอ offline ก็เก็บ data แล้วส่งตอน online ทีหลัง ทำให้เราไม่ต้องโหลดข้อมูลจาก API ทุกครั้ง

ทำไมถึงบอกว่าใช้ท่าเดียวกันกับเรียกใช้ API ได้หล่ะ

เพราะว่าใช้ DAO เหมือนกันหล่ะสิ DAO คืออะไร DAO คือ Data Access object เว่าแบบง่ายๆก็คือ object ของ data นั่นแหละ ว่า data ก้อนนี้ มีอะไรบ้าง ในส่วนของ model class ตัวอย่างจาก codelab

//Book.java
@Entity
public class Book {
public @PrimaryKey String id;
public String title;
}

และมี interface เรียกใช้งานในส่วน query data แต่เปลี่ยนจาก API เป็น SQL เอง
ข้อควรมีสติ คือ ใส่คำสั่ง SQL ให้ถูกต้อง ไม่เช่นนั้น build ไม่ผ่านนะเออ

//BookDao.java
@Query("SELECT * FROM Book " +
"INNER JOIN Loan ON Loan.book_id = Book.id " +
"INNER JOIN User on User.id = Loan.user_id " +
"WHERE User.name LIKE :userName"
)
public LiveData<List<Book>> findBooksBorrowedByName(String userName);

basic function ของ persistent storage ตามหลักการของ boyband CRUD ประกอบด้วย Create, Read (Retrieve), Update (Modify), และ Delete (Destroy) และใน DAO จะใช้ในการแสดงข้อมูล ค้นหาข้อมูล และเปลี่ยนแปลงข้อมูล ดังนี้

  • @Query
  • @Insert
  • @Delete
  • @Update

ในการ query data เราต้องคำนึงถึง entity relationship ว่า ไม่ต้องรู้ว่าเราคบกันแบบไหน เอ้ยยยยย มีความสัมพันธ์แบบไหน

  • 1-to-1 เช่น ISBN ของหนังสือ ซึ่งจะมีเลขซํ้ากันไม่ได้
  • 1-to-many เช่น นักเขียนคนนี้ เขียนหนังสือมาแล้วกี่เล่ม
  • many-to-many เช่น สำนักพิมพ์นี้นักเขียนคนไหนบ้าง และนักเขียนคนเดียวก็เขียนหนังสือให้หลายสำนักพิมพ์

เราสามารถใช้ function query data ที่ถูกสร้างใน DAO ณ ViewModel class ซึ่ง คืนค่าออกมาเป็น LiveData และ ViewModel ก็ถูกเรียกใช้ใน Activity อีกทีนึง

ด้วยความที่บางครั้ง บางคราว เราก็จะคืน type คนละแบบกับ input
ดังนั้นเราจะใช้ type convertor เข้าช่วย

public class DateConverter {
@TypeConverter
public static Date toDate(Long timestamp) {
return timestamp == null ? null : new Date(timestamp);
}

@TypeConverter
public static Long toTimestamp(Date date) {
return date == null ? null : date.getTime();
}
}

ถ้าไม่ใช้หล่ะ (ไม่ใส่ @TypeConverter) จะเป็นแบบนี้ build ไม่ผ่านจ้า

ถ้าไม่ใส่ TypeConverter ก็จะเป็นแบบนี้แหละ

หลังจากการลองเล่นใน codelab ซึ่งก็ยังงงๆกันอยู่ ถือว่าเป็นแบบ beginner เลยนะ ทำความเข้าใจ concept และการใช้งานขั้นต้น

อ้าว แล้วเอาไปประยุกต์ใช้ยังไง ในเมื่อแต่ละแอปก็โครงสร้างอลังการประมาณนึง และ เรียกใช้ API ทุกทีเมื่อมีโอกาสเสียด้วย แบบนี้

ก็เรียกใช้ API ต่อไปนั่นแหละ แต่มันจะอยู่ในอีก module นึง เรียกว่า repository
เช่น เรียก backend service หรือ REST API จาก retrofit เป็นต้น

ดังนั้น เราสร้าง class Repository ขึ้นมา แล้วใช้ retrofit เรียกปกตินั่นแหละ

การมี Repository class จะช่วยลดความซํ้าซ้อนของโค้ดเราได้ ซึ่งหลักการก็เหมือน MVVM นั่นแหละ ตัว Repository จะเชื่อมต่อกับ webservice ทั้งหลายนั้นใช้คำสั่งอะไรพวกนี้เหมือนกัน แต่ url ของ API ต่างกับ Object ที่ได้ก็ต่างกัน เลยลดความซํ้าซ้อนในส่วนนี้ไม่ได้ แล้วทำยังไงดีหล่ะ มี 2 วิธีที่ช่วยแก้ปัญหานี้ คือ Dependency Injection การทำให้ส่วนต่างๆใน class เป็นอิสระต่อกัน และ Service Locator แยกแต่ละส่วนของ class เป็นชิ้นๆ เพื่อเรียก service ของอันนั้นๆ ถ้ายังงงๆ บล็อกด้านล่างเขียนเข้าใจง่ายมากๆ และใช้ Dagger 2 จัดการพวก dependencies ทั้งหลาย

การ cache data ก็เป็นเรื่องสำคัญ จะให้แอปเราโหลดใหม่ทุกครั้งก็ไม่ไหวเนอะ เราจะเก็บ cache ไว้ที่ Repository นั่นแหละ และการ persisting data เราจะใช้ Room ในการเก็บ

โครงสร้างที่น่ารักจะเป็นแบบนี้แหละ

credit : https://developer.android.com/topic/libraries/architecture/guide.html#recommended_app_architecture

แล้วทำ testing อย่างไรดีหล่ะ ก็ทำเหมือนเคย UI Test ก็ใช้ Espresso ถ้า ViewModel Repository ก็ใช้ JUint ประมาณนี้

เหมือนจะจบแต่ยังไม่จบนะ ยังมี Paging Library

เราๆก็รู้ดีนะว่า หน้าๆนึงของแอปนี่นะ กว่าจะได้หน้านึงมา ต้องโหลดอะไรเข้ามาบ้าง ซึ่งบางทีข้อมูลจาก backend ก็ไม่ได้เล็กไง Paging Library จะช่วยทำให้แอปของเราโหลดข้อมูลขึ้นได้ง่ายขึ้น ไม่ต้องโหลดซํ้า หรือรอโหลดนานๆ ปกติจะใช้ CursorAdapter ช่วยในการ query database มาแสดงใน ListView หรือ AsyncListUtil ใน RecyclerView แต่ตอนนี้มี Room ที่สามารถใช้ Paging Library เพื่อทำ paging ให้ง่ายขึ้น ถ้าเราใช้ Paging Library ร่วมกับวิธีปกติที่ไม่ใช่ Room ได้ไหม ก็ได้นะ มันไม่ได้แบบบังคับว่าต้องใช้ร่วมกันไง แล้วแต่เราเลย

ส่วน flow การทำงานก็จะเป็นในลักษณะนี้ คือ ถ้ามี item ใหม่มาเพิ่มใน database ของเรานั้น

credit : https://developer.android.com/topic/libraries/architecture/paging.html#classes

DataSource ของ item ที่เพิ่มมาใหม่ จะถูกส่งไปที่ PagedList และส่งเข้ามาใน PagedListAdapter เพื่อเตรียมนำมาแสดงผลในหน้า UI ของเรา ใช้ DiifUtil เป็น background thread เพื่อหาที่แทรกใน list สำหรับ item ใหม่ และพอทำงานเสร็จก็จะ onBindViewHolder เพื่อแสดงผลในแอปเรา

สรุปภาพโค้ดของ Paging Library โดย Room

ทำ DAO ในการ query data บน database ของเรา

จากนั้นใช้ ViewModel เพื่อดึง output ที่ได้จากการ query มาไว้ที่ตัวแปรที่เป็น LiveData

นำ ViewModel มาแสดงผลใน Activity/Fragment บน RecyclerView

ซึ่งใช้ PageListAdapter ก็จะมีการ binding view ตามปกติ และเพิ่ม DiffCallBack เข้ามา เพื่อ check ข้อมูลที่ได้มา ถ้าต่างกันก็จะมีการ reload ใหม่

แต่น่าเสียดายที่ไม่มี codelab ของ Paging Library แหะ

ถ้าโปรเจกเราจะเปลี่ยนจาก JAVA เป็น Kotlin มันจะมีปัญหาไหม คิดว่าไม่มีนะ ลองอ่านตามนี้ดู

ถ้ากับ Instant apps หล่ะ อันนี้ยังไม่มีคำตอบนะ เพิ่งลองเล่นเอง
เลยขอตัดจบเจอกันตอนหน้าเลยแล้วกันค่ะ >_<

ถามว่าทำไมเราต้องศึกษาเรื่องนี้หล่ะ?

ด้วยความที่ทางทีม product ของฟังใจ ให้ความสำคัญเรื่อง clean code และในส่วนของแอนดรอยด์ มีการปรับปรุงโครงสร้างของโค้ด จากที่แบ่งโฟลเดอร์เป็น activity fragment adapter ประมาณนี้ มาเป็นแบบ feature แทน เช่น artist, playlist, player และไม่อยากจะเขียนอะไรที่ซํ้าซ้อนกัน บาง view ก็ใช้ตัวเดียวกันไปเลย หรือรวบเป็น class เดียว

เอาจริงๆคนที่ใช้แอปฟังใจในแอนดรอยด์ก็เห็นปัญหาบางอย่าง เช่น หน้าแรกโหลดนานมาก ถ้าไม่ใช้ 4G WiFi

ดังนั้นเราจะต้องหา solution ในการทำให้แอปของเรา performance ดีขึ้น และทำให้ทีม happy ในการปรับเปลี่ยน code ที่ทำให้ทีมง่ายต่อการทำความเข้าใจ และทำงานได้เร็วขึ้น และทางทีมเห็นว่า Architecture Component สามารถช่วยในการปรับปรุง performance แอปให้ดีขึ้น จึงศึกษาความเป็นไปได้ และออกมาเป็นบล็อกนี้ค่ะ :)

สุดท้าย finally ขอบคุณคำชี้แนะจาก perfessional ทั้งสองท่านค่ะ พี่แชมป์ Champ AK และพี่เอก Ake Exorcist ที่ทำให้บทความนี้สมบูรณ์มากขึ้นนะคะ

--

--

Minseo Chayabanjonglerd
Fungjai

Android Developer | Content Creator AKA. MikkiPastel | Web2 & Web3 Contributor