มาใช้ Android Architecture Components กันเถอะ

Ta Theerasan Tonthongkam
ta tonthongkam
Published in
6 min readJul 3, 2017

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

3 ปัจจัยหลักเลยที่ทำให้โค้ดมีคุณภาพได้แก่

  1. Readable
  2. Maintainable
  3. Testable

3 ปัจจัยนี้สามารถเป็นตัวชี้วัดได้อย่างดีเลยว่าเราเขียนโค้ดได้เฉียบแค่ไหน — การออกแบบ Architecture ที่ดีควรตอบโจทย์เหล่านี้นะ บทความนี้จะไม่ขอพูดถึงเรื่อง Pattern มากนัก แต่จะอ้างจาก pattern ที่ Google นำเสนอ โดยเสนอเกี่ยวกับ Android Architecture Components เป็นหลัก

Architecture ที่ Google แนะนำ

Disclaimer : บทความนี้จะพูดถึงข้อดี ข้อเสียเป็นหลัก ยังไม่มีตัวอย่างโค้ดมากนัก (ถ้ามีเวลาว่าง จะเขียนอีกบทความเป็น Example ของ Android Architecture Components)

Android Architecture Components มีอะไรใหม่มาให้เล่นบ้าง?

ViewModel

แผนภาพแสดงการทำงานระหว่าง View, ViewModel และ Binding

คือ class ใดๆ ก็ตามที่ Extend ViewModel() ทำหน้าที่ในการจัดการข้อมูล ที่นำมาแสดงใน UI ซึ่ง ViewModel จะต้องติดต่อกับ Business Logic เพื่อ รับ/ส่ง หรือ Update ค่า การที่จะส่งค่า หรืออัพเดทค่าต่างๆ ได้ควรทำผ่านการ Binding เราจะได้ลดการเขียนโค้ดในชั้น View (Activity, Fragment ViewGroup etc.) เราจะได้ไม่ต้องเรียกคำสั่ง setText setColor หรือ set ใดๆ อีก แค่ให้ ViewModel จัดการให้

แต่ก่อนหน้านี้ Google เองก็ Provide Class BaseObservable() มาให้ ซึ่งสามารถทำหน้าที่ได้ประการเดียวกัน แล้วของใหม่ดีกว่ายังไงหล่ะ?

1 .ไม่ต้อง notifyPropertyChange(BR.value)

ในกรณี setter เราไม่ต้อง Notify Property Change อีกต่อไปแล้ว ทำให้โค้ดสั้นลงมากมาย ตัวอย่างเช่น

//ใช้ class BaseObservable()
class UserViewModel : BaseObservable() {
var username: String? = ""
set(username) {
notifyPropertyChange(BR.username)
}
var password: String? = ""
set(password) {
notifyPropertyChange(BR.password)
}

}
//ใช้ class ViewModel()
class UserViewModel : ViewModel() {
var username: ObservableField<String>? = ObservableField()
var password: ObservableField<String>? = ObservableField()
}

notifyPropertyChange ถือเป็นหนึ่งในโค้ดขยะเลยก็ว่าได้ เพราะมันต้อง Notify ทุกครั้งที่มีการ Set ค่า ถ้าเรากำจัดมันออกได้โค้ดจะดูสั้นลงเยอะเลย แล้วทำให้โค้ดเข้าใจง่ายขึ้น และลดโอกาสการลืม Notify Property ด้วย

2. ความสามารถในการ แชร์ ViewModel

ข้อดีอีกข้อคือ ความสามารถในการ แชร์ ViewModel ระหว่าง Fragment กับ Fragment หรือ ระหว่าง Activity กับ Fragment

ก่อนหน้านี้ ถ้าเราอยากจะแชร์ Data เราคงนึกถึง singleton โดยการสร้าง instance ของ ViewModel ขึ้นมาแสดงแล้วส่งข้อมูลหากัน

ภาพประกอบไม่เกี่ยวกับเนื้อหา

แต่ในบางสถานการณ์ singleton กลับเป็นดาบสองคมเช่น เรามี UserViewModel ซึ่งหน้า Login และ หน้าโปรไฟล์ จำเป็นต้องใช้ UserViewModel ที่แชร์ข้อมูลต่อกัน แต่เรามีหน้า FriendList ด้วย ที่ใช้ UserViewModel แต่ข้อมูลไม่แชร์กับ ViewModel ตัวอื่นๆ ก่อนหน้านี้ทางออกคงจะเป็น สร้าง FriendViewModel ขึ้นมาอีกอัน แล้ว copy and paste จาก UserViewModel — แต่ class ViewModel สามารถ ทำเรื่องดังกล่าวได้ง่ายๆ

//ViewModel ที่ต้องการจะแชร์
class UserViewModel : ViewModel() {
//implement here
}
//class ที่ต้องการแชร์ ViewModel
class LoginFragment : Fragment() {
val model = ViewModelProviders.of(activity as
MainActivity).get(UserViewModel::class.java)
}
//class ที่ต้องการแชร์ ViewModel
public class UserProfileFragment : Fragment() {
val model = ViewModelProviders.of(activity as
MainActivity).get(UserViewModel::class.java)
}
//class ที่ไม่ต้องการแชร์ ViewModel
public class FriendFragment : Fragment() {
val model = ViewModelProviders.of(this)
.get(UserViewModel::class.java);
}

ถ้าดูจากโค้ด Fragment ที่ต่างจากคนอื่นคือ FriendFragment — แต่มันตรงไหน
เหรอ?

ข้อสังเกต ViewModelProviders.of(this) ViewModel มันถูก Provide มาจากคนละ Context กันมันเลยไม่แชร์ Data กัน แต่ UserViewModel กับ LoginFragment มันได้ ViewModel มาจาก Context เดียวกัน [Context จาก Main Activity] ค่าที่ได้จึงเหมือนกันทุกประการ แล้วที่สำคัญกว่านั้น มันถูกสร้างแบบ Lazy (สร้างตอนใช้) แล้วก็ Sync กับ Lifecycle ของ Android เลย เราไม่จำเป็นต้องมา Handle singleton กับ Lifecycle อีกต่อไป

3. ความสามารถของ state retaining

ก่อนหน้านี้ถ้าเราต้องการทำ App ให้หมุนจอได้เราต้องจัดการเรื่อง SaveInstanceState ซึ่งถ้าข้อมูลที่ต้องการบันทึกมี 20 อย่าง แค่คิดจะ SaveInstanceState หรือ RestoreInstanceState ก็ เหนื่อยแล้ว แต่ ViewModel มันสามารถเก็บค่าต่างๆ ไว้ได้ อย่างที่บอกว่า ViewModel จะ Sync กับ Lifecycle อัตโนมัติ และมันจะตายก็ต่อเมื่อ Context โดน destroy

การหมุนจอ Activity มันจะเข้า Lifecycle ดังนี้ onSaveInstanceState → onPause → onStop → onCreate → onStart → onRestoreInstanceState → onResume จะเห็นได้ว่า Activity ไม่โดนทำลาย context ก็ยังอยู่ ViewModel หรอ ก็อยู่สิ!! และมันก็จัดแจง Restore state เอง ในนาทีนี้ขอมอบสโกแกนนี้ให้เลย

จะพลิกสักที่ท่า ก็ไม่ต้องกลัวข้อมูลรั่วซึม ด้วย ViewModel

LiveData, Mutable LiveData

LiveData คือ Class ที่เก็บข้อมูลต่างๆ แต่อนุญาตให้คนอื่นเข้าสังเกตการณ์ได้ ซึ่งมีความต่างกับ Observer ทั่วไปอยู่ ตรงที่มันแคร์ Lifecycle ของ Android มากนะเออ มันจะจัดการให้เองว่าตอนไหนควร observe หรือตอนไหยควรหยุด

จะขอยกตัวอย่างง่ายๆ LiveData คือพี่อ้อยพี่ฉอด สองคนนี้หน้าที่ไม่มีอะไรมาก เพียงรอคนโทรศัพท์เข้ามาเล่าเรื่อง จากนั้นเอาเรื่องที่ได้มา ไปทำ Club Friday the Series แล้วสองคนนี้จะคอย observe เฉพาะตอนรายการออนแอร์เท่านั้น ไม่ต้องรอให้ใครมาบอก เห็นมะเข้าใจง่ายจะตาย

ลองดูโค้ดกัน

class UserStoryViewModel : ViewModel() {
//สร้างพี่อ้อยพี่ฉอดขึ้นมา
val userStory = LiveData<User>()
}

การเรียกใช้

//พี่อ้อยพี่ฉอดเพียงรอคนโทรมา จากนั้นก็สร้าง Club Friday The Series
viewModel.userStory.observe(this, user -> {
createClubFridayStory(user)
})

note: จริงๆ แล้ววิธีการใช้ LiveData คอนเซ็ปท์แทบจะไม่ต่างกับ RxJava หรือ Agera เลย มันสามารถใช้แทนกันได้ แต่ข้อเสียคือ เราต้องมาจัดการ Lifecycle เอง เพื่อป้องกัน Memory leak

ส่วน Mutable LiveData ก็เหมือนกับ LiveData ยกเว้นความสามารถในการแปลงร่าง (Mutant) [ตัว Live Data ไม่มี Method setter สำหรับ Value] และสามารถทำ data binding กับ View ได้อีกด้วย วิธีใช้มักจะใช้คู่กับ Service ViewModel และ Binding

private var userData = MutableLiveData<UserDTO>()fun login(username: String, password: String): LiveData<UserDTO> {
service?.login(username, password)?
.enqueue(object : Callback<UserDTO> {
override fun onFailure(call: Call<UserDTO>?, t: Throwable?) {
//do something else
}
override fun onResponse(call: Call<UserDTO>?, response:
Response<UserDTO>?) {
userData.value = when (response?.code()) {
200 -> response.body()
else -> //return something else
}
}
})
return userData
}
<TextView
android:layout_width=”wrap_content”
android:layout_height=”wrap_content”
android:text=”@{viewModel.userDto.get().firstName}” />

จากโค้ดจะเห็นได้ว่า method login จะ ส่ง Mutable Live Data ของ User ออกมา โดยรอ Response จาก Server เมื่อได้ Response ก็แค่อัพเดท Mutable หลังจากนั้น View ก็ อัพเดทเอง แต่ถ้าเราใช้ Live Data แทน Mutable Live Data เราต้องมา อัพเดท View เองตรง .observe() แต่เราไม่ต้องทำแบบนั้นอีกแล้ว และนี่คือประโยชน์หลักๆ ของ Mutable Live Data

หลังจากนี้เราสามารถเขียนโปรแกรมโดยลดความกังวลเกี่ยวกับ Lifecycle ได้ระดับนึง แต่ไม่ได้หมายความว่าเราจะไม่ใส่ใจ Lifecycle อีกแล้วนะ

Room Persistence Library

Room provides คือ Abstract Layer ของ SQLite เพื่ออณุญาตให้เราเข้าถึง แล้วจัดการ Database ได้อย่างรวดเร็ว และเต็มประสิทธิภาพตาม SQLite

ท่า Room จากเรื่อง One piece

Room ทำอะไรได้บ้าง?

Room จะจัดการ 3 components เหล่านี้ให้ ในขณะที่เราพยายาม Provide abstraction layer ของ SQLite.

  1. Database แค่ extend RoomDatabase() จากนั้นก็จะได้ class ที่จัดการเกี่ยวกับ Database ให้เรา แถมสามารถเลือกได้เลยว่า เราอยากจะใช้ Local Database หรือ In Memory Database จาก คำสั่ง Room.databaseBuilder() หรือ Room.inMemoryDatabaseBuilder(). ตามลำดับ
  2. Entity เข้าใจง่ายๆ คือ Class ที่ถือ data ของ Tableใน 1 แถว
  3. DAO เป็น Class ที่จัดการ Data Access Object (DAO) ซึ่งจะเป็นตัวกำหนด SQL method ที่จะเข้าถึง Database
@Entity
public class User {
@PrimaryKey
private int uid;
@ColumnInfo(name = “first_name”)
private String firstName;
@ColumnInfo(name = “last_name”)
private String lastName;
// Getters and setters are ignored for brevity,
// but they’re required for Room to work.
}
@Dao
public interface UserDao {
@Query(“SELECT * FROM user”)
List<User> getAll();
@Query(“SELECT * FROM user WHERE uid IN (:userIds)”)
List<User> loadAllByIds(int[] userIds);
@Query(“SELECT * FROM user WHERE first_name LIKE :first AND “
+ “last_name LIKE :last LIMIT 1”)
User findByName(String first, String last);
@Insert
void insertAll(User… users);
@Delete
void delete(User user);
}
@Database(entities = {User.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract UserDao userDao();
}

แค่ สร้าง Class ต่างๆ ข้างบน แค่นี้ก็สามารถสร้าง Database ได้แล้วจากคำสั่ง

AppDatabase db = Room.databaseBuilder(getApplicationContext(),AppDatabase.class, “database-name”).build();

ประโยชน์ของ Room

ในกรณีที่ App เราต้องการทำ Caching การมี Local Database ถือเป็นเรื่องที่ดีทีเดียว แต่มันอาจจะไม่จำเป็นขนาดนั้นในเมื่อ Library อื่นๆ เช่น Retrofit เองก็สามาร Caching Response ได้ ถ้า App เราไม่จำเป็นขนาดที่ต้องสร้าง Database จริงๆ แนะนำว่าใช้วิธีอื่นในการทำ Caching ดีกว่านะประหยัดเวลากว่าเยอะ

ViewModel LiveData Room คือ component ใหม่ เกิดมาเพื่อตอบโจทย์คนทำ Android ที่เคยเจอปัญหาต่างๆ เห็น การ Retain ค่าของ fragment, การแชร์ข้อมูลต่อกัน, การทำ Caching ฯลฯ แน่นอนว่าเราเห็นข้อดีมันเยอะมาก เพราะมันเกิดมาเพื่อแก้ปัญหาเดิมๆ ที่เคยมี แต่มันยังมีข้อเสียซ่อนอยู่เช่นกันนะ

ข้อควรระวังในการใช้ Android Architecture Components

Alpha Version

Component เหล่านี้ยังคงเป็น Alpha Version อยู่ มันอาจจะยังไม่ Stable มากนัก การเอาเข้ามาใช้จริงใน Production อาจจะก่อให้เกิดปัญหาได้ แต่ถ้า Application อยู่ใน สถานะ in progress แล้วจะ Release ใน 3–4 เดือนข้างหน้า ก็น่าจะลองที่เอาเข้ามาใช้

Spoil

มันจัดการเรื่อง Life cycle และ Observable ให้เยอะมาก จนบางครั้งเอาอาจลืมคอนเซ็ปท์ดั้งเดิมมันไป ฉนั้นการใช้ Components เหล่านี้ เราควรเข้าใจพื้นฐานจริงๆก่อน

Observable Field กับ 2 Ways Blinding ที่หายไป

ใน class ViewModel ตัวแปรต่างๆ ใน Class จะต้องเป็น ObservableField, ในกรณี EditText ที่เดิมทีสามารถเขียน 2 Ways Binding ได้ ตามโค้ดข้างล่าง

class LoginViewModel : ViewModel() {
var username: ObservableField<String>? = ObservableField()
}
<AutoCompleteTextView
android:inputType=”textEmailAddress”
android:maxLines=”1"
android:hint=”@string/prompt_email”
android:text=“@={viewModel.username}”
android:imeOptions=“actionNext”/>

แต่!! — เพราะว่า username มันไม่สามารถ Set String ได้อีกแล้ว มันต้อง Set เป็น ObservableField<String> แทน เราจึง ไม่สามารถ เขียนโค้ดแบบนี้ได้อีก android:text=“@={viewModel.username}” ต้องแก้เป็น

android:text=“@{viewModel.username}”

*note จริงๆ เราสามารถเขียนเครื่องหมาย “=” ได้แบบเดิมได้ ไม่ Compile error แต่อย่างใด แค่ค่าจะไม่โดน Set กลับไปอีกแล้ว

เราต้องใช้วิธีดั้งเดิมในการ check OnChange() เหมือนเดิม หรือมีอีกวิธีนึง ซึ่งจะเขียนใน Blog ต่อไป

สรุป

Android Architecture Components ออกแบบมาเพื่อ “บังคับ” ให้ Developer เขียนโปรแกรมเป็น Pattern มากขึ้น และแก้ปัญหาหลายๆ อย่างที่เคยเกิดขึ้นในการพัฒนา Android ในยุคก่อนๆ เช่น การหมุนจอ การแชร์ข้อมูล การทำ Caching แต่มันก็มีสิ่งที่ต้องแลกมา คือควรเสียเวลาเรียนรู้มันจริงๆ และเข้าใจปัญหาเดิมๆ ที่เคยมี อย่าโดน Spoil เพราะรู้ว่ามันดีกว่าแค่นั้น เราจะได้สร้างแอพพลิเคชันที่มีคุณภาพได้ และเข้าในพื้นฐานมันจริงๆ

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

สุดท้ายนี้ ในฐานะที่ทำ Android มาสักพักนึง และเข้าใจปัญหาต่างๆ ของการพัฒนา Android ผมแนะนำให้เริ่มหันมาใช้ Android Architecture Components ดีกว่าครับ เพราะได้ประโยชน์มากกว่าสิ่งที่เราต้องเสียสละไป เรียกว่าคุ้มค่าจริงๆ ในเมื่อประโยชน์มันเยอะขนาดนี้ ก็ขอให้คุณอำนาจของพระศรีรัตนตรัยช่วยบรรดาลให้ Google ไม่เท Android Architecture Components ด้วยเถอะ — สาธุ

--

--