iT邦幫忙

2026 iThome 鐵人賽

DAY 19
0
Software Development

AI 時代的 Android Engineer:30 天打造我的 AI 開發工作流系列 第 19 篇

Day 19 - 都是launch,為什麼Coroutine放在哪個Scope差這麼多?

  • 分享至 

  • xImage
  •  

前幾天在整理 ViewModel、Process Death 和 State Ownership 時,一直在問同一件事:

這份 State 應該跟誰一起活,又跟誰一起消失?

昨天先岔出去看了 Repository,今天想回到這條線,但把問題從「資料」換成「正在執行的工作」。

Android 現在很常用 Coroutine,平常也很常看到:

viewModelScope.launch {
    ...
}

或:

lifecycleScope.launch {
    ...
}

寫久了很容易變成一種習慣:

ViewModel
→ viewModelScope

Fragment
→ lifecycleScope

但如果只是記住「在哪裡就用哪個 Scope」,其實會漏掉一個更重要的問題:

為什麼這個 Coroutine 應該活這麼久?

Coroutine 啟動之後,也需要有人負責結束它

假設我要取得資料:

scope.launch {
    val data = repository.getData()
    updateUi(data)
}

這個 Coroutine 啟動後可能只跑 100ms,也可能因為網路、等待或其他原因跑很久。

問題是,如果使用者已經離開畫面:

開始取得資料
↓
使用者離開
↓
原本畫面消失
↓
資料才回來

這份工作還需不需要繼續?

答案不一定。

有些工作畫面消失後就沒有意義。

有些工作即使畫面重建,也應該繼續。

甚至有些工作離開 App 之後仍然需要可靠完成。

所以 Coroutine 的生命週期其實不能只看:

我現在在哪個 Class 裡?

而是要看:

這份工作真正屬於誰?

viewModelScope 為什麼可以跨過畫面重建?

先看最常見的:

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 就好了嗎?

Fragment 裡很常看到:

lifecycleScope.launch {
    ...
}

它會跟 Fragment 的 Lifecycle 綁在一起。

大致可以理解成:

Fragment
↓
Lifecycle
↓
lifecycleScope
↓
Fragment DESTROYED
↓
Coroutine Cancel

看起來很合理。

但 Fragment 有一個比較特別的地方:

Fragment 的 Lifecycle 和 Fragment View 的 Lifecycle 不是同一個東西。

這也是前幾天講 View Lifecycle 時碰過的問題。

Fragment 還活著,不代表 View 還活著

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 {
    ...
}

lifecycleScope 和 viewLifecycleOwner.lifecycleScope 差在哪?

名字很像,但 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 之後,這份工作本來就沒有繼續存在的理由。

所以不是 Fragment 一律用 lifecycleScope

如果只記:

ViewModel
→ viewModelScope

Fragment
→ lifecycleScope

其實還是太粗。

應該再多問一層:

這個工作屬於 Fragment 本身?

還是只屬於 Fragment 的 View?

例如:

取得並保存畫面資料
→ 可能屬於 ViewModel

操作目前的 Fragment View
→ 跟 View Lifecycle 關係更直接

Fragment 本身需要完成的工作
→ 才考慮 Fragment Lifecycle

所以 Scope 不是根據「Code 寫在哪裡」選的。

比較像是根據:

這份工作什麼時候失去意義?

來選。

自己建立 CoroutineScope 又差在哪?

如果寫:

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。

我覺得更重要的是:

它們幫這份工作建立了一個明確的生命週期。

Scope 其實也是 Ownership

這時候就會發現,它跟前幾天整理 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,本身就已經回答了一個很重要的問題。

我現在不會先問「這裡要用哪個 Scope」

如果現在看到一段:

launch {
    ...
}

以前可能先想:

這裡是在 ViewModel 還是 Fragment?

現在我反而會先問:

這份工作是誰發起的?

它真正屬於誰?

畫面重建後還需要繼續嗎?

View 消失後還有意義嗎?

Owner 消失時,它是不是應該一起被取消?

回答完這些,再去選:

viewModelScope

lifecycleScope

viewLifecycleOwner.lifecycleScope

或其他更適合的機制

會比較有意義。

前幾天一直在整理:

State 應該跟誰一起活?

今天換成 Coroutine,我覺得其實只是把同一個問題套到另一種東西上:

這份工作應該跟誰一起活?

所以 viewModelScope、lifecycleScope 不只是方便的 Coroutine API。

它們其實是在 Code 裡直接表達:

這份工作的 Owner 是誰,以及它應該活到什麼時候。

當我開始從這個角度看 Scope,就比單純記住「ViewModel 用 viewModelScope、Fragment 用 lifecycleScope」更容易知道自己為什麼這樣寫。


上一篇
Day 18 - Repository 到底在幹嘛?只是幫ViewModel包一層DAO嗎?
下一篇
Day 20 - 都用了lifecycleScope,為什麼collect Flow還要repeatOnLifecycle?
系列文
AI 時代的 Android Engineer:30 天打造我的 AI 開發工作流 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言