11 May 2019

[Android Code] จะใช้ ViewModel หรือ AndroidViewModel ดี?



        ผู้ที่หลงเข้ามาอ่านหลายๆคนคงคุ้นเคยกับ ViewModel ใน Android Architecture Component กันมาพอสมควรแล้ว ซึ่งจะมีทั้งคลาส ViewModel และ AndroidViewModel ให้เรียกใช้งาน

        อ้าว!? แล้วมันต่างกันยังไงล่ะ?


ต่างกันที่ Constructor

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

class AwesomeViewModel : ViewModel() {
    // Do something
}

class AwesomeAndroidViewModel(app: Application) : AndroidViewModel(app) {
    // Do something
}

        แต่ ViewModel ไม่ต้องโยนอะไรให้

แต่สร้างด้วยวิธีที่เหมือนกัน

       ถึงแม้ว่า AndroidViewModel จะต้องมีคลาส Application อยู่ใน Constructor แต่ตอนที่สร้างด้วย ViewModelProviders นั้นยังคงเหมือนกับ ViewModel เป๊ะๆ

val viewModel = ViewModelProviders.of(activity).get(AwesomeViewModel::class.java)
val androidViewModel = ViewModelProviders.of(activity).get(AwesomeAndroidViewModel::class.java)

        โดยคลาส Application ที่ AndroidViewModel จะถูกโยนเข้ามาให้โดยอัตโนมัติ

AndroidViewModel สามารถดึง Context มาใช้งานได้ แต่ ViewModel ไม่มีให้เรียกใช้งาน

        เนื่องจาก AndroidViewModel มีคลาส Application อยู่ข้างใน จึงทำให้นักพัฒนาสามารถดึง Application Context หรือ Base Context มาใช้งานได้ตามต้องการ

class CustomAndroidViewModel(app: Application) : AndroidViewModel(app) {
    fun doSomething() {
       val context: Context = getApplication<Application>().applicationContext
       // Do something
    } 
}

        แต่ถ้าเป็น ViewModel จะไม่สามารถทำแบบนี้ได้ จึงต้องใช้วิธีโยน Context เข้ามาตอนเรียก Method นั้นๆแทน

class CustomViewModel : ViewModel() {
    fun doSomething(context: Context) {
       // Do something
    } 
}

         ดังนั้นความแตกต่างของทั้ง 2 คลาสนี้ก็จะอยู่ตรงที่การเรียกใช้งาน Context นั่นเอง

ข้อจำกัดในการเรียกใช้งาน Context ของ AndroidViewModel

        ถึงแม้ว่า AndroidViewModel จะเรียกใช้ Context ได้สะดวกสบายมากกว่าก็จริง แต่ไม่ควรใช้ Context เพื่อดึงค่าต่างๆที่เกี่ยวข้องกับ Configuration Qualifier เด็ดขาด เพราะตอนที่เกิด Configuration Changes จะมีแค่ Activity เท่านั้นที่ถูกทำลายแล้วสร้างขึ้นมาใหม่ ในขณะที่ ViewModel จะยังคงทำงานร่วมกับ Activity ตัวนั้นต่อจากเดิมได้จนกว่าจะหยุดทำงานแล้วถูกทำลายจริงๆ


        สาเหตุที่ Activity ต้องถูกทำลายแล้วสร้างขึ้นมาใหม่ เมื่อเกิด Configuration Changes ก็เพราะว่า Configuration ใน Context มีการเปลี่ยนแปลง จึงต้องทำลาย Activity ตัวเก่าทิ้งแล้วสร้างขึ้นมาใหม่เพื่อให้ Context ที่อยู่ใน Activity เป็นตัวใหม่ (ไม่สามารถแก้ไข Context ที่อยู่ใน Activity ได้โดยตรง)

        แต่ทว่า AndroidViewModel ไม่ได้ถูกทำลายในตอนที่เกิด Configuration Changes ดังนั้น Context ที่อยู่ใน AndroidViewModel ก็จะเป็นของเก่าอยู่ และเมื่อเอาไปดึงข้อมูลต่างๆที่เกี่ยวข้องกับ Configuration Qualifier ก็จะทำให้ดึงข้อมูลผิดในทันที

        ยกตัวอย่างเช่น Activity ถูกสร้างขึ้นมาโดยกำหนดเป็นภาษาอังกฤษ (EN) ไว้ จึงทำให้ AndroidViewModel ถูกสร้างขึ้นมาพร้อมกับ Context ที่กำหนดภาษาเป็นภาษาอังกฤษไว้ แต่เมื่อมีการเปลี่ยนภาษาเกิดขึ้นจึงทำให้ Activity ถูกทำลายและสร้างขึ้นมาใหม่โดยที่ Context ข้างใน Activity ตัวนั้นเป็นภาษาไทย (TH) แทน แต่ทว่า AndroidViewModel ที่ไม่ได้ถูกทำลายจะยังคงถือ Context ตัวเดิมอยู่ ดังนั้นเวลาดึงค่าจาก String Resource ใน AndroidViewModel ก็จะได้เป็นข้อความภาษาอังกฤษ (EN) ตลอดเวลา

class CustomAndroidViewModel(app: Application) : AndroidViewModel(app) {
    fun getGreetingMessage(isNewUser: Boolean): String {
       val context: Context = getApplication<Application>().applicationContext
       return when (isNewUser) { 
           true -> context.getString(R.string.new_user_greeting_message)
           false -> context.getString(R.string.old_user_greeting_message
       }
    } 
}

        ดังนั้นเวลาใช้ AndroidViewModel จะต้องระมัดระวังเรื่องนี้ให้ดีๆ

แล้วกรณีแบบนี้ควรแก้ปัญหายังไง?

         การดึงข้อมูลที่เกี่ยวข้องกับ Configuration Qualifier ควรทำใน Activity หรือ Fragment แทน เพื่อให้ได้ Context ที่อัปเดตใหม่ล่าสุดเสมอ

         ดังนั้นการส่ง ID ของ String Resource กลับไปแทนจึงเป็นวิธีที่ปลอดภัยกว่า

class CustomAndroidViewModel(app: Application) : AndroidViewModel(app) {
    fun getGreetingMessage(isNewUser: Boolean): Int = when (isNewUser) {
        true -> R.string.new_user_greeting_message
        false -> R.string.old_user_greeting_message
    }
}

        แล้วค่อยให้ปลายทางที่เป็น Activity หรือ Fragment ใช้คำสั่ง getString(...) จาก Context แทน

แล้ว AndroidViewModel ควรจะใช้เมื่อไร?

         ควรใช้เมื่อต้องการดึงข้อมูลต่างๆที่ไม่เกี่ยวข้องกับ Configuration Qualifier ยกตัวอย่างเช่น System Service ต่างๆของระบบแอนดรอยด์

class CustomAndroidViewModel(app: Application) : AndroidViewModel(app) {
    fun getLastKnownLocation(): Location {
       val context: Context = getApplication<Application>().applicationContext
       val locationManager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
       return locationManager.getLastKnownLocation("gps")
    } 
}

สรุป

        ถึงแม้ว่า AndroidViewModel จะสะดวกตรงที่สามารถดึง Context จาก Application เพื่อใช้งานข้างในคลาสได้ทันที แต่ควรเลี่ยงการดึงข้อมูลใดๆก็ตามที่เกี่ยวข้องกับ Configuration Qualifier ถ้าจำเป็นต้องดึงข้อมูลดังกล่าวก็ควรให้คำสั่งอยู่ในระดับ Activity หรือ Fragment แทน

         ส่วน ViewModel จะเหมาะกับการใช้งานทั่วๆไปที่ไม่ได้ต้องการ Context มากกว่า โดยเฉพาะการทำ Dependency Injection ที่แทบจะไม่ได้เรียก Context โดยตรง เพราะว่าคลาสที่ต้องการใช้งาน Context จะถูก Inject เข้ามาใน ViewModel อยู่แล้ว

        ดังนั้นถ้าจะใช้ AndroidViewModel ก็ดูให้ดีล่ะ ถ้าไม่จำเป็นก็ใช้ ViewModel แทนดีกว่านะ