前幾天在整理 ViewModel、Process Death 和 State Ownership 時,一直在問同一件事:
這份 State 應該跟誰一起活,又跟誰一起消失?
昨天先岔出去看了 Repository,今天想回到這條線,但把問題從「資料」換成「正在執行的工作」。
Android 現在很常用 Coroutine,平常也很常看到:
viewModelScope.launch {
...
}
或:
lifecycleScope.launch {
...
}
寫久了很容易變成一種習慣:
ViewModel
→ viewModelScope
Fragment
→ lifecycleScope
但如果只是記住「在哪裡就用哪個 Scope」,其實會漏掉一個更重要的問題:
為什麼這個 Coroutine 應該活這麼久?
假設我要取得資料:
scope.launch {
val data = repository.getData()
updateUi(data)
}
這個 Coroutine 啟動後可能只跑 100ms,也可能因為網路、等待或其他原因跑很久。
問題是,如果使用者已經離開畫面:
開始取得資料
↓
使用者離開
↓
原本畫面消失
↓
資料才回來
這份工作還需不需要繼續?
答案不一定。
有些工作畫面消失後就沒有意義。
有些工作即使畫面重建,也應該繼續。
甚至有些工作離開 App 之後仍然需要可靠完成。
所以 Coroutine 的生命週期其實不能只看:
我現在在哪個 Class 裡?
而是要看:
這份工作真正屬於誰?
先看最常見的:
class ProductViewModel(
private val repository: ProductRepository
) : ViewModel() {
fun loadProduct() {
viewModelScope.launch {
val product = repository.getProduct()
// 更新 State
}
}
}
viewModelScope 跟 ViewModel 的生命週期綁在一起。
所以如果只是 Configuration Change:
Old Activity / Fragment
↓
被重新建立
ViewModel
↓
仍然存在
viewModelScope 裡的工作
↓
可以繼續
這就跟 Day 15 講 ViewModel 保存 State 的原因很像。
假設 API Request 原本就是 ViewModel 負責:
ViewModel
↓
Repository
↓
API
螢幕旋轉時,沒必要因為 UI 被重建,就把 Request 取消後重新送一次。
所以 viewModelScope 不只是:
ViewModel 裡面開 Coroutine 比較方便的寫法。
它其實還表達了一個重要的設計:
這份工作的生命週期屬於 ViewModel。
當 ViewModel 被 clear 時,viewModelScope 裡的工作也會被取消。
Fragment 裡很常看到:
lifecycleScope.launch {
...
}
它會跟 Fragment 的 Lifecycle 綁在一起。
大致可以理解成:
Fragment
↓
Lifecycle
↓
lifecycleScope
↓
Fragment DESTROYED
↓
Coroutine Cancel
看起來很合理。
但 Fragment 有一個比較特別的地方:
Fragment 的 Lifecycle 和 Fragment View 的 Lifecycle 不是同一個東西。
這也是前幾天講 View Lifecycle 時碰過的問題。
Fragment 可能出現:
Fragment 還存在
↓
onDestroyView()
↓
View 被銷毀
↓
之後再次 onCreateView()
所以才會常看到 ViewBinding:
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
因為 binding 指向的是 View。
View 消失後,就不應該繼續持有它。
那 Coroutine 其實也有同樣的問題。
假設:
lifecycleScope.launch {
val data = repository.getData()
binding.title.text = data.title
}
這份工作是跟 Fragment Lifecycle 綁在一起。
但它最後操作的是:
Fragment View
如果中間 View 已經被 Destroy,而 Fragment 本身還沒有 Destroy,就可能出現生命週期不一致。
真正跟這份工作有關的 Owner,其實是:
Fragment View
而不是 Fragment 本身。
所以才會有:
viewLifecycleOwner.lifecycleScope.launch {
...
}
名字很像,但 Owner 不一樣。
Fragment.lifecycleScope
↓
跟 Fragment Lifecycle
viewLifecycleOwner.lifecycleScope
↓
跟 Fragment View Lifecycle
也就是:
Fragment
├── Fragment Lifecycle
│ ↓
│ lifecycleScope
│
└── View Lifecycle
↓
viewLifecycleOwner.lifecycleScope
如果這份工作只在目前這個 View 存在時才有意義,那跟 View Lifecycle 綁在一起會比較符合它真正的需求。
例如:
viewLifecycleOwner.lifecycleScope.launch {
...
}
當 onDestroyView() 發生,對應 Lifecycle Destroy,這個 Scope 裡的工作也會被取消。
這時就不只是「避免 Crash」而已。
更重要的是:
沒有 View 之後,這份工作本來就沒有繼續存在的理由。
如果只記:
ViewModel
→ viewModelScope
Fragment
→ lifecycleScope
其實還是太粗。
應該再多問一層:
這個工作屬於 Fragment 本身?
還是只屬於 Fragment 的 View?
例如:
取得並保存畫面資料
→ 可能屬於 ViewModel
操作目前的 Fragment View
→ 跟 View Lifecycle 關係更直接
Fragment 本身需要完成的工作
→ 才考慮 Fragment Lifecycle
所以 Scope 不是根據「Code 寫在哪裡」選的。
比較像是根據:
這份工作什麼時候失去意義?
來選。
如果寫:
CoroutineScope(Dispatchers.Main).launch {
...
}
本身不是不能用。
問題是:
誰負責 Cancel?
例如:
private val scope = CoroutineScope(
SupervisorJob() + Dispatchers.Main
)
那通常也代表我需要自己決定:
scope.cancel()
要在哪裡發生。
如果忘了處理,就可能變成:
Owner 已經不存在
↓
Coroutine 還在執行
Android 提供 viewModelScope、lifecycleScope 這些 Scope 的其中一個好處,就是它們已經把 Coroutine cancellation 跟 Android Lifecycle 接起來。
所以與其說它們是:
官方幫我少寫一點 Coroutine Code。
我覺得更重要的是:
它們幫這份工作建立了一個明確的生命週期。
這時候就會發現,它跟前幾天整理 State 很像。
State 的問題是:
誰擁有這份 State?
↓
Owner 應該活多久?
↓
State 跟著活多久?
Coroutine 其實也是:
誰擁有這份工作?
↓
Owner 應該活多久?
↓
Coroutine 跟著活多久?
例如:
ViewModel
↓
viewModelScope
↓
ViewModel cleared
↓
Cancel
或:
Fragment View
↓
viewLifecycleOwner.lifecycleScope
↓
View Destroy
↓
Cancel
所以 Scope 不是單純決定:
Coroutine 在哪裡跑。
它還決定:
這份工作的生命週期邊界在哪裡。
講到這裡其實還有下一個問題。
假設 Fragment View 還存在,但 App 已經切到背景:
View 沒 Destroy
↓
Lifecycle STOPPED
↓
使用者現在看不到畫面
如果我正在:
viewLifecycleOwner.lifecycleScope.launch {
flow.collect {
render(it)
}
}
viewLifecycleOwner 還沒有 Destroy,所以這個 Coroutine 不會只因為畫面進入 STOPPED 就自動結束。
這就代表:
跟 Lifecycle 一起 Destroy,和只在 Lifecycle 處於特定狀態時執行,是兩件不同的事情。
這也是為什麼收集 Flow 時,還會看到:
repeatOnLifecycle(...)
但這部分我想留到下一篇再拆。
因為光是 Scope,本身就已經回答了一個很重要的問題。
如果現在看到一段:
launch {
...
}
以前可能先想:
這裡是在 ViewModel 還是 Fragment?
現在我反而會先問:
這份工作是誰發起的?
它真正屬於誰?
畫面重建後還需要繼續嗎?
View 消失後還有意義嗎?
Owner 消失時,它是不是應該一起被取消?
回答完這些,再去選:
viewModelScope
lifecycleScope
viewLifecycleOwner.lifecycleScope
或其他更適合的機制
會比較有意義。
前幾天一直在整理:
State 應該跟誰一起活?
今天換成 Coroutine,我覺得其實只是把同一個問題套到另一種東西上:
這份工作應該跟誰一起活?
所以 viewModelScope、lifecycleScope 不只是方便的 Coroutine API。
它們其實是在 Code 裡直接表達:
這份工作的 Owner 是誰,以及它應該活到什麼時候。
當我開始從這個角度看 Scope,就比單純記住「ViewModel 用 viewModelScope、Fragment 用 lifecycleScope」更容易知道自己為什麼這樣寫。