Android State Changes - State Handling in Activity
ข้อมูลใด ๆ ที่เก็บไว้ใน Activity ควรมีการทำ State Handling เพื่อป้องกันข้อมูลหายจาก Configuration Changes และ Process Recreation ซึ่งเป็นหนึ่งในพื้นฐานสำคัญสำหรับการพัฒนาแอปบนแอนดรอยด์
ในปัจจุบันจะไม่นิยมเก็บข้อมูลจำพวก UI State ไว้ใน Activity โดยตรง ควรเก็บไว้ใน ViewModel ตามรูปแบบของ App Architecture ที่ทีมแอนดรอยด์แนะนำ
บทความในชุดเดียวกัน
- Introduction
- Configuration Changes
- Process Recreation
- State Handing in Activity [Now Reading]
- State Handing in Fragment
- State Handing in View
- State Handing in ViewModel
- State Handing in Compose
เมื่อเกิด State Changes ใน Activity
จากที่เคยอธิบายไปในบทความตอนก่อนหน้านี้ว่า ในระหว่างที่เกิด State Changes สิ่งที่เกิดขึ้นกับ Activity ก็คือ Activity Recreation ที่จะทำลาย (Destroy) Activity ที่แสดงผลอยู่ แล้วสร้างขึ้นมาใหม่ (Recreate)
ดังนั้นสิ่งที่เกิดขึ้นในมุมของ Activity Lifecycle ก็คือ
- Destroy –
onPause
→onStop
→onDestroy
- Recreate –
onCreate
→onStart
→onResume
ซึ่งเป็นลำดับการทำงานของ Activity Lifecycle แบบที่นักพัฒนาคุ้นเคยกันดีนั่นเอง
การทำ State Handling ใน Activity
ใน Activity จะมี Override Method สำหรับเหตุการณ์ทั้ง 2 เพื่อช่วยให้นักพัฒนาสามารถเก็บข้อมูลจำพวก UI State ให้รอดพ้นจาก State Changes ได้
onSaveInstanceState
– ทำงานก่อน Activity จะถูกทำลาย (Destroy)onRestoreInstanceState
– หลังจาก Activity ถูกสร้างขึ้นใหม่ (Recreate)
โดย Method ทั้งสองจะส่ง Bundle เข้ามาให้เพื่อให้นักพัฒนาเก็บข้อมูลไว้ในนั้นและคืนค่ากลับมาเพื่อให้ Activity ทำงานต่อจากเดิมได้
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private var name: String? = null
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putString("name", name)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
name = savedInstanceState.getString("name")
// Restore UI state
}
}
จะเห็นว่าการทำ State Handling ใน Activity จะใช้หลักการเดียวกับการส่งข้อมูลระหว่าง Activity กับ Activity ที่จะต้องเก็บข้อมูลไว้ใน Bundle และนั่นหมายความว่าข้อมูลที่นักพัฒนาจะในระหว่างที่เกิด State Changes จะต้องเป็นข้อมูลจำพวก Primitive Data, Parcelable, หรือ Serializable เท่านั้น
ข้อมูลทั้งหมดที่เก็บไว้ใน Bundle ไม่ควรมีขนาดเกิน 1MB
สามารถ Restore หรือคืนข้อมูลตอน onCreate ได้
จากตัวอย่างโค้ดก่อนหน้าจะเห็นว่าขั้นตอนการ Restore หรือคืนข้อมูลจะเกิดขึ้นใน onRestoreInstanceState
แต่ในความเป็นจริงนั้นนักพัฒนาก็สามารถคืนข้อมูลในตอน onCreate
ได้เช่นกัน
เพราะถ้ายังจำกันได้ ใน onCreate
จะมี Parameter ที่ชื่อว่า savedInstanceState
ส่งเข้ามาให้ด้วย ซึ่งเป็นข้อมูลตัวเดียวกับใน onRestoreInstanceState
นั่นเอง ดังนั้นนักพัฒนาจึงกำหนดด้วยตัวเองได้ว่าอยากจะคืนข้อมูลในตอนไหน
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { /* ... */ }
override fun onRestoreInstanceState(savedInstanceState: Bundle) { /* ... */ }
}
ส่วนใหญ่จะแนะนำให้ทำใน onRestoreInstanceState
เพราะเข้าใจได้ง่ายกว่า
และนอกจากนี้นักพัฒนาสามารถเช็คจาก savedInstanceState
ที่อยู่ใน onCreate
ได้อีกด้วยว่า Activity ตัวนี้ถูกสร้างขึ้นมาครั้งแรกหรือถูกสร้างขึ้นมาใหม่เพราะ State Changes โดยเช็คว่าค่าดังกล่าวเป็น null
หรือไม่
// MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
if (savedInstanceState == null) {
// Activity เพิ่งถูกสร้างขึ้นมา
} else {
// Activity ถูกสร้างขึ้นมาใหม่ (Recreate) หลังจากเกิด State Changes
}
}
}
ทำให้ใน onCreate
จะไม่นิยมคืนข้อมูลจาก savedInstanceState
กัน แต่จะใช้เช็คว่า Activity ถูกสร้างขึ้นมาตอนไหน เพื่อเรียกคำสั่งที่แตกต่างกัน
เพื่อให้เห็นภาพมากขึ้น ขอยกตัวอย่างเป็นการทำงานของ Activity ที่จำเป็นต้องโหลดข้อมูลจาก Web Service แบบนี้
ตัวอย่างที่ 1 – ต้องการให้ Activity โหลดข้อมูลใหม่ทุกครั้ง
ข้อมูลทั้งหมดอยู่ที่ Web Service และไม่ได้มีข้อมูลใหม่เกิดขึ้นจากการทำงานของ Activity
ในกรณีนี้นักพัฒนาไม่จำเป็นต้องทำ State Handling ใน Activity ก็ได้ เพราะข้อมูลจะถูกโหลดใหม่ทั้งหมดเสมอ
class MainActivity : AppCompatActivity() {
private var name: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
getNameFromWebService { result ->
name = result.name
}
}
}
ถึงแม้จะเกิด State Changes ในระหว่างนี้ สิ่งที่เกิดขึ้นกับ Activity ตัวนี้ก็คือจะถูก Recreate และทำการโหลดข้อมูลใหม่ทั้งหมดจาก Web Service
ตัวอย่างที่ 2 – ต้องการให้ Activity โหลดข้อมูลใหม่เฉพาะตอนแรกสุดเท่านั้น
ข้อมูลทั้งหมดอยู่ที่ Web Service ก็จริง แต่ไม่อยากให้โหลดข้อมูลใหม่ทุกครั้ง ถ้าเกิด State Changes ก็ให้ใช้ข้อมูลของเดิมที่เก็บไว้
ในกรณีนี้ให้ทำ State Handling ใน Activity เพื่อจะได้ไม่ต้องเสียเวลาโหลดข้อมูลใหม่ในตอน State Changes
class MainActivity : AppCompatActivity() {
private var name: String? = null
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
if (savedInstanceState == null) {
getNameFromWebService { result ->
name = result.name
// Do something with `name`
}
}
}
override fun onSaveInstanceState(outState: Bundle) {
/* ... */
outState.putString("name", name)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
/* ... */
name = savedInstanceState.getString("name")
// Restore UI state
}
}
นอกจาก State Handling แล้ว จะต้องเช็คค่า savedInstanceState
ใน onCreate
ด้วย เพื่อโหลดข้อมูลจาก Web Service แค่ครั้งแรกสุดเท่านั้น
ปัญหาของ Asynchronous Operation จาก Configuration Changes
จากตัวอย่างที่อธิบายไปก่อนหน้า ถ้าคำสั่ง getNameFromWebService(...)
ทำงานเสร็จก่อนที่จะเกิด Configuration Changes ก็จะทำงานได้ปกติ เพราะได้ Response จาก Web Service มาเก็บไว้ในตัวแปร name
ที่มีการทำ State Handling ไว้แล้ว
แต่ถ้าคำสั่งดังกล่าวทำงานอยู่ แล้วเกิด Configuration Changes ในระหว่างนั้นพอดี จะเจอปัญหาว่า Activity ที่ถูก Recreate จะได้ไม่ได้ข้อมูลที่มาจากคำสั่งนั้น เพราะการทำงานของคำสั่งนั้นผูกอยู่กับ Activity ตัวก่อนหน้าที่ถูกทำลายไปแล้ว
เพื่อแก้ปัญหาด้วยวิธีที่ดีที่สุด นักพัฒนาควรย้ายการทำงานที่เป็น Asynchronous Operation เหล่านี้และเก็บข้อมูลไว้ใน ViewModel แทน เพราะ ViewModel เป็น Component ที่จะไม่ถูกทำลายเมื่อเกิด Configuration Changes
// MainViewModel.kt
class MainViewModel(
private val api: WebServiceApi,
) : ViewModel() {
private var _name: MutableStateFlow<String?> = MutableStateFlow(null)
val name: StateFlow<String?> = _name
fun getNameFromWebService() {
api.getNameFromWebService { result ->
_name.update { result.name }
}
}
}
แล้วส่งข้อมูลกลับไปให้ Activity ในรูปของ State Flow ที่เป็นความสามารถของ Coroutine ในภาษา Kotlin
// MainActivity.kt
class MainActivity : AppCompatActivity() {
private val viewModel: MainViewModel by viewModel()
override fun onCreate(savedInstanceState: Bundle?) {
/* ... */
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
state.collect { name ->
// Do something with `name`
}
}
}
if (savedInstanceState == null) {
viewModel.getNameFromWebService()
}
}
}
repeatOnLifecycle
เป็นคำสั่งที่อยู่ในandroidx.lifecycle:lifecycle-runtime-ktx
วิธีนี้จะช่วยให้ Activity สามารถโหลดข้อมูลจาก Web Service ได้ ถึงแม้ว่าจะเกิด Configuration Changes ในระหว่างนั้น และถ้าต้องการให้ข้อมูลที่อยู่ใน ViewModel ยังคงอยู่ในตอนที่เกิด Process Recreation ก็ให้ทำ State Handling ที่ ViewModel แทน
สรุป
ถึงแม้ว่าในปัจจุบันจะไม่นิยมเก็บข้อมูลจำพวก UI State ไว้ใน Activity กันแล้ว แต่ถ้านักพัฒนามีความจำเป็นต้องเก็บข้อมูลไว้ใน Activity ก็ควรจะทำ State Handling ให้ถูกต้อง เพื่อให้ Activity สามารถทำงานต่อได้อย่างราบรื่นถึงแม้ว่าจะเกิด State Changes ในระหว่างนั้นก็ตาม