viewModelScope เป็น CoroutineScope ในตัวจากไลบรารี androidx.lifecycle ที่เชื่อมโยงกับวงจรชีวิตของ ViewModel และถูกยกเลิกโดยอัตโนมัติเมื่อถูกล้าง ตามข้อมูลจาก Google Android Developers, 2025 viewModelScope เป็นกลไกมาตรฐานสำหรับเริ่มคอรูทีนในสถาปัตยกรรม MVVM ซึ่งรับประกันการทำงานแบบอะซิงโครนัสที่ปลอดภัยโดยไม่มีความเสี่ยงในการรั่วไหลของหน่วยความจำ ViewModelScope ใช้ Dispatchers.Main เป็นค่าเริ่มต้น และการดำเนินการ IO ทั้งหมดภายในจะต้องดำเนินการผ่าน withContext
ประเด็นสำคัญ
viewModelScope เป็นคุณสมบัติส่วนขยายบนอินเทอร์เฟซ ViewModel ที่ถูกเพิ่มในไลบรารี lifecycle-viewmodel-ktx (ตั้งแต่เวอร์ชัน 2.1.0) โดยให้ CoroutineScope ที่พร้อมใช้งานเชื่อมโยงกับวงจรชีวิตของ ViewModel
// Internal structure (simplified)
val ViewModel.viewModelScope: CoroutineScope
get() {
val scope = this.getTag(JOB_KEY)
if (scope != null) return scope
return CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
.also { setTag(JOB_KEY, it) }
}
scope ถูกสร้างแบบขี้เกียจ (lazy) เมื่อเข้าถึงครั้งแรกและถูกแคชผ่าน setTag โดยใช้ SupervisorJob ซึ่งหมายความว่าข้อยกเว้นในคอรูทีนย่อยหนึ่งจะไม่ยกเลิกคอรูทีนอื่น ตัวจัดส่งค่าเริ่มต้นคือ Dispatchers.Main.immediate ซึ่งดำเนินการโค้ดบนเธรดหลักโดยไม่ต้องจัดส่งเพิ่มเติมหากอยู่บน Main อยู่แล้ว
เมื่อ ViewModel ออกจากวงจรชีวิต (Activity สิ้นสุดลงหรือ Fragment ถูกลบออก) ระบบจะเรียก clear() ซึ่งเรียกใช้ onCleared() ในคอลแบ็กนี้ viewModelScope จะยกเลิก Job ของมัน ซึ่งจะสิ้นสุดคอรูทีนที่ทำงานอยู่ทั้งหมดแบบเรียกซ้ำ กลไกนี้ถูกนำไปใช้ผ่านอินเทอร์เฟซ Closeable โดยที่ Job ของ scope ถูกลงทะเบียนเป็นทรัพยากรสำหรับการปิดอัตโนมัติ
กลไกการเชื่อมโยง viewModelScope กับวงจรชีวิตของ ViewModel ขึ้นอยู่กับการติดแท็กและคอลแบ็ก onCleared มาดูทีละขั้นตอน
เมื่อ ViewModel ดำเนินการ viewModelScope.launch { ... } getter จะตรวจสอบว่ามี scope ที่ถูกเก็บไว้ภายใต้แท็ก JOB_KEY หรือไม่ หากไม่มี scope จะสร้างอินสแตนซ์ใหม่ของ CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate) scope ถูกเก็บไว้ภายใน ViewModel ผ่านแผนที่แท็กภายใน
คอรูทีนทั้งหมดที่เริ่มผ่าน viewModelScope.launch หรือ viewModelScope.async จะกลายเป็นลูกของ SupervisorJob ของ scope พวกมันทำงานบนเธรดหลัก (เว้นแต่จะระบุตัวจัดส่งอื่นผ่าน withContext) ตราบใดที่ ViewModel ยังทำงานอยู่ คอรูทีนสามารถทำงาน ถูกระงับ หรือเสร็จสมบูรณ์
เมื่อระบบทำลาย ViewModel จะเรียก ViewModel.clear() ภายใน clear() จะเกิดสิ่งต่อไปนี้:
เมื่อหน้าจอหมุน Activity จะถูกสร้างใหม่ แต่ ViewModel ยังคงอยู่ (ต้องขอบคุณ ViewModelStoreOwner) ซึ่งหมายความว่า viewModelScope ยังคงทำงานอยู่และคอรูทีนดำเนินการต่อไปอย่างไม่ขาดตอน หลังจากสร้าง Activity ใหม่ ViewModel เดียวกัน (และ scope เดียวกัน) จะถูกนำกลับมาใช้ใหม่ — การโหลดข้อมูลจะไม่เริ่มต้นใหม่
MVVM (Model-View-ViewModel) เป็นสถาปัตยกรรมที่ Google แนะนำสำหรับแอปพลิเคชัน Android viewModelScope มีบทบาทสำคัญในฐานะผู้ดำเนินการทำงานแบบอะซิงโครนัส
| ชั้น | ส่วนประกอบ | บทบาทของ viewModelScope |
|---|---|---|
| UI | Activity / Fragment | สังเกต StateFlow/LiveData จาก ViewModel |
| ViewModel | ViewModel | เริ่มคอรูทีนผ่าน viewModelScope จัดการสถานะ UI |
| Repository | Repository | เปิดเผยฟังก์ชัน suspend ที่ถูกเรียกจากคอรูทีนของ viewModelScope |
| Data | DAO / Api | ดำเนินการคำขอจริง (Room, Retrofit) |
ViewModel เริ่มคอรูทีนผ่าน viewModelScope ซึ่งภายในจะเรียก ฟังก์ชัน suspend ของ Repository ผลลัพธ์ถูกแปลงเป็น StateFlow ซึ่งถูกสังเกตโดยชั้น UI การออกแบบนี้รับประกันการแบ่งแยกหน้าที่ชัดเจนและความสามารถในการทดสอบอิสระของแต่ละชั้น
หากคอรูทีนถูกเริ่มจาก Fragment พวกมันจะถูกยกเลิกเมื่อหมุนหน้าจอพร้อมกับการทำลาย Fragment ViewModel อยู่รอดการหมุน ดังนั้นคอรูทีนที่เริ่มใน scope ของมันจะดำเนินการต่อ นี่คือข้อได้เปรียบหลักของ viewModelScope เหนือ lifecycleScope เมื่อโหลดข้อมูล
มาดูสามสถานการณ์เชิงปฏิบัติของการใช้ viewModelScope ในแอปพลิเคชัน Android ด้วย Kotlin
class ProfileViewModel(
private val repo: ProfileRepository
) : ViewModel() {
private val _profile = MutableStateFlow<Profile?>(null)
val profile: StateFlow<Profile?> = _profile
init {
loadProfile()
}
private fun loadProfile() {
viewModelScope.launch {
val result = repo.getProfile()
_profile.value = result
}
}
}
ในบล็อก init การโหลดโปรไฟล์จะเริ่มทันที คอรูทีนทำงานบนเธรดหลัก (โดยค่าเริ่มต้น) พื้นที่เก็บข้อมูลใช้ withContext(Dispatchers.IO) สำหรับคำขอเครือข่ายภายในฟังก์ชัน suspend ดังนั้น ViewModel ไม่ต้องกังวลเกี่ยวกับการสลับเธรด
sealed class UiState {
object Loading : UiState()
data class Success(val data: List<Item>) : UiState()
data class Error(val message: String) : UiState()
}
fun fetchItems() {
_state.value = UiState.Loading
viewModelScope.launch {
try {
val items = repo.getItems()
_state.value = UiState.Success(items)
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
สถานะ UI ถูกอธิบายผ่าน sealed class UiState ViewModel อัปเดตสถานะทุกครั้งที่มีการเปลี่ยนแปลง Fragment สมัครรับ StateFlow และตอบสนองต่อสถานะปัจจุบันเท่านั้น โดยไม่สนใจการเรียกที่ล้าสมัยจากการหมุนครั้งก่อน
private var searchJob: Job? = null
fun search(query: String) {
searchJob?.cancel()
searchJob = viewModelScope.launch {
delay(300)
val results = repo.search(query)
_searchResults.value = results
}
}
ในแต่ละคำค้นหาใหม่ คอรูทีนก่อนหน้าจะถูกยกเลิก delay(300) ใช้ดีบาวซ์ — การค้นหาจะดำเนินการหลังจากไม่มีการเคลื่อนไหว 300 มิลลิวินาทีเท่านั้น ซึ่งลดภาระของเซิร์ฟเวอร์และป้องกันผลลัพธ์ที่ล้าสมัย
ทั้งสอง scope ถูกให้โดยไลบรารี AndroidX Lifecycle แต่เชื่อมโยงกับวงจรชีวิตที่แตกต่างกัน การเลือกขึ้นอยู่กับประเภทของงาน
| คุณลักษณะ | viewModelScope | lifecycleScope |
|---|---|---|
| เจ้าของ | ViewModel | LifecycleOwner (Activity/Fragment) |
| ยกเลิกเมื่อหมุน | ไม่ (ViewModel อยู่รอด) | ใช่ (Activity ถูกสร้างใหม่) |
| ตัวจัดส่งค่าเริ่มต้น | Dispatchers.Main.immediate | Dispatchers.Main.immediate |
| พร้อมใช้งานใน | ViewModel | Activity, Fragment, Service |
| กรณีใช้งานทั่วไป | การโหลดข้อมูล ตรรกะทางธุรกิจ | การโต้ตอบกับ UI แอนิเมชัน |
Google แนะนำให้ใช้ viewModelScope สำหรับงานโหลดและประมวลผลข้อมูลทั้งหมด ควรใช้ lifecycleScope สำหรับการดำเนินการที่เชื่อมโยงกับช่วงเวลาเฉพาะของวงจรชีวิต UI — ตัวอย่างเช่น เริ่มแอนิเมชันเมื่อปรากฏหน้าจอครั้งแรก หรือสมัครรับการอัปเดตตำแหน่งที่ควรหยุดเมื่อออกจากหน้าจอ
แม้ใน Android API ที่มีเอกสารครบถ้วน นักพัฒนาก็ยังทำข้อผิดพลาดทั่วไป มาดูสี่ปัญหาที่พบบ่อยที่สุด
ข้อผิดพลาดที่ร้ายกาจที่สุดคือการพยายามอัปเดต StateFlow หรือ LiveData หลังจาก ViewModel ถูกล้าง แม้ว่า viewModelScope จะถูกยกเลิกที่ onCleared() คอรูทีนอาจดำเนินการโค้ดก่อนที่การยกเลิกจะมีผล ใช้ isActive เพื่อตรวจสอบหรือพึ่งพาการเสร็จสมบูรณ์ของบล็อก catch
viewModelScope ใช้ SupervisorJob ภายใน ซึ่งแยกข้อผิดพลาดระหว่างคอรูทีน อย่างไรก็ตาม หากคุณเริ่มคอรูทีนด้วย Job() ของตัวเองภายใน viewModelScope.launch คอรูทีนนั้นจะกลายเป็นลูกของ SupervisorJob แต่จะไม่ได้รับการป้องกันจากการยกเลิกที่เกิดจากข้อผิดพลาดในคอรูทีนอื่น
แม้ว่า viewModelScope จะไม่มีข้อจำกัดที่เข้มงวด แต่คอรูทีนที่ทำงานอยู่หลายพันตัวอาจทำให้ระบบช้าลง สำหรับรายการข้อมูลยาว ให้ใช้ Flow กับ collectLatest แทนการสร้างคอรูทีนแยกสำหรับแต่ละรายการ
หากนำเข้า GlobalScope โดยไม่ได้ตั้งใจแทน viewModelScope คอรูทีนจะไม่ถูกยกเลิกเมื่อ ViewModel ถูกล้าง ซึ่งนำไปสู่การรั่วไหลของหน่วยความจำและอาจทำให้แอปพลิเคชันล้มเหลว ตรวจสอบให้แน่ใจเสมอว่าคอรูทีนเริ่มผ่าน viewModelScope โดยเฉพาะในคลาสย่อยของ Fragment
คำถามที่พบบ่อย
คุณไม่สามารถเปลี่ยนตัวจัดส่งของ viewModelScope ได้โดยตรง — มันถูกกำหนดตายตัวเป็น Dispatchers.Main.immediate อย่างไรก็ตาม ภายในคอรูทีนคุณสามารถสลับไปยังตัวจัดส่งอื่นผ่าน withContext หากต้องการเปลี่ยนตัวจัดส่งในการทดสอบ ให้ใช้ TestDispatcher ผ่าน Rule
อย่าส่ง scope ไปยัง Repository — ซึ่งละเมิดหลักการทางสถาปัตยกรรม Repository ควรเปิดเผย ฟังก์ชัน suspend และ ViewModel จัดการคอรูทีนผ่าน viewModelScope หาก Repository ต้องการ scope ให้พิจารณาสถาปัตยกรรมใหม่เป็น Clean Architecture
SupervisorJob รับประกันว่าข้อยกเว้นในคอรูทีนหนึ่ง (เช่น ข้อผิดพลาดในการโหลดในหนึ่งในคำขออิสระหลายรายการ) จะไม่ยกเลิกคอรูทีนอื่น ซึ่งสอดคล้องกับสถานการณ์ ViewModel ที่หน้าจอต่างกันโหลดข้อมูลอิสระ
ใช่ viewModelScope พร้อมใช้งานใน ViewModel ใดๆ โดยไม่ขึ้นกับประเภท UI (View System หรือ Jetpack Compose) ใน Compose คอรูทีนก็เริ่มผ่าน viewModelScope เช่นกัน ในขณะที่เอฟเฟกต์ UI ใช้ LaunchedEffect และ rememberCoroutineScope
การเรียก viewModelScope.cancel() จะยกเลิก scope ทันที — คอรูทีนที่ทำงานอยู่ทั้งหมดสิ้นสุดด้วย CancellationException หากเรียก viewModelScope.launch หลังจากนั้น scope ใหม่จะถูกสร้างโดยอัตโนมัติเมื่อเข้าถึง getter ครั้งถัดไป
สรุป
เราจะพัฒนาแอปพลิเคชันบนมือถือแบบครบวงจร
IT Sectr สร้างแอปพลิเคชัน iOS และ Android สำหรับสตาร์ทอัพและธุรกิจตั้งแต่ปี 2017 เราจะให้คำแนะนำและเสนอวิธีแก้ปัญหาที่ดีที่สุดแก่คุณ
อ่านเพิ่มเติม