iT邦幫忙

2026 iThome 鐵人賽

DAY 20
0
Software Development

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

Day 20 - 都用了lifecycleScope,為什麼collect Flow還要repeatOnLifecycle?

  • 分享至 

  • xImage
  •  

昨天整理 Coroutine Scope 時,最後留了一個問題。

假設 Fragment 裡已經這樣寫:

viewLifecycleOwner.lifecycleScope.launch {
    viewModel.uiState.collect { state ->
        render(state)
    }
}

viewLifecycleOwner.lifecycleScope 不是已經跟 View Lifecycle 綁在一起了嗎?

View 被 Destroy,Coroutine 就會取消。

那為什麼現在收集 Flow 時,還很常看到:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(
        Lifecycle.State.STARTED
    ) {
        viewModel.uiState.collect { state ->
            render(state)
        }
    }
}

第一次看其實會覺得有點繞。

明明都已經有 lifecycleScope 了,為什麼還需要再包一層?

關鍵其實是:

「什麼時候整個 Coroutine 應該結束」和「什麼時候這段工作應該執行」是兩個不同的問題。

lifecycleScope 管的是「活到什麼時候」

先看最簡單的:

viewLifecycleOwner.lifecycleScope.launch {
    ...
}

這代表 Coroutine 跟 Fragment View 的 Lifecycle 綁在一起。

大致可以想成:

View CREATED
↓
Coroutine 啟動
↓
STARTED
↓
RESUMED
↓
STOPPED
↓
Coroutine 還可能存在
↓
View DESTROYED
↓
Coroutine Cancel

重點在最後:

Lifecycle 到 DESTROYED 時,Scope 裡的 Coroutine 才會被取消。

但 Android 畫面並不是只有:

存在
不存在

兩種狀態。

中間還有:

CREATED
STARTED
RESUMED

例如使用者按 Home:

Fragment View 還存在
↓
畫面進入背景
↓
Lifecycle 不再處於 STARTED

這時 View 沒有被 Destroy。

所以 viewLifecycleOwner.lifecycleScope 本身也不會因為畫面暫時看不到,就把整個 Scope Cancel。

這對一般 Coroutine 不一定是問題。

但如果這份工作的意義是:

只有畫面可見時才需要收集 UI State。

那就還少了一個條件。

collect 本身不會知道 UI 現在看不看得到

假設 ViewModel 有:

val uiState: StateFlow<UiState>

Fragment:

viewLifecycleOwner.lifecycleScope.launch {
    viewModel.uiState.collect { state ->
        render(state)
    }
}

只要這個 Coroutine 還活著,collect 就會持續等待新的資料。

所以可能變成:

畫面 STARTED
↓
collect

使用者切到背景
↓
畫面 STOPPED
↓
collect 還在

State 更新
↓
collector 仍收到資料

但使用者現在根本看不到這個畫面。

這時就會出現一個問題:

這些 UI 更新現在真的有必要做嗎?

如果只是簡單的 State,也許影響不明顯。

但 Flow 可能持續更新,甚至上游還牽涉其他工作。

所以 Lifecycle-aware collection 不只是避免 Crash,也是在決定:

什麼時候這份資料值得被 UI 收集。

repeatOnLifecycle 多管了一層「現在是不是該執行」

這時 repeatOnLifecycle 的角色就比較清楚了。

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(
        Lifecycle.State.STARTED
    ) {
        viewModel.uiState.collect { state ->
            render(state)
        }
    }
}

可以粗略理解成:

Lifecycle >= STARTED
↓
啟動裡面的 Coroutine
↓
開始 collect

Lifecycle < STARTED
↓
取消裡面的 Coroutine
↓
停止 collect

再次 >= STARTED
↓
重新啟動
↓
重新 collect

所以它不是把外面的 lifecycleScope 取代掉。

兩層其實負責不同事情:

viewLifecycleOwner.lifecycleScope
↓
View Destroy 時,整個工作結束

repeatOnLifecycle(STARTED)
↓
只有 Lifecycle 至少在 STARTED 時
裡面的工作才存在

這樣看就比較不會覺得是在重複做同一件事。

Cancel 之後,回來不是「繼續」,而是重新 Collect

這點也很重要。

假設:

STARTED
↓
開始 collect

STOPPED
↓
collector Cancel

STARTED
↓
建立新的 collector

它不是把原本的 Coroutine 暫停在那裡,回來再從原本那一行繼續。

而是:

離開指定 Lifecycle State 時取消,回來之後重新執行 block。

這也是 repeat 這個名字的意思。

所以放在裡面的工作,應該要能接受:

開始
↓
取消
↓
再次開始

StateFlow 為什麼很適合這種模式?

假設 ViewModel:

private val _uiState = MutableStateFlow(
    UiState()
)

val uiState = _uiState.asStateFlow()

Fragment 第一次進入 STARTED:

collect
↓
拿到目前的 State

使用者切到背景:

STOPPED
↓
停止 collect

但 ViewModel 的 StateFlow 本身還是持有目前最新的值。

假設背景期間:

State A
↓
State B
↓
State C

Fragment 不需要把每一次畫面更新全部補做一遍。

重新回到 STARTED:

重新 collect
↓
取得目前最新的 State C
↓
重新 render

這其實很符合 UI State 的特性。

UI 通常真正需要的是:

現在應該長什麼樣子?

而不是:

我不在畫面上的期間,總共錯過了幾次 render?

這也是 StateFlow 拿來表示 UI State 時很容易理解的一個地方。

但 Event 就開始不太一樣了

這裡也會順便碰到另一個問題。

假設 Flow 裡不是:

目前畫面 State

而是:

顯示 Toast
Navigation
開啟 Dialog

這些比較像一次性的 Event。

如果 collector 在 STOPPED 時被取消,那這段期間發生的 Event:

回來之後到底應不應該再收到?

答案就不像 State 那麼單純。

例如:

畫面進背景
↓
發出 Navigation Event
↓
沒有 collector
↓
使用者回來

現在應該:

補做 Navigation?

還是:

直接忽略?

這已經不是 repeatOnLifecycle 本身能替我們決定的事情。

因為這是產品行為和資料模型的問題。

所以:

State

和:

Event

雖然都可以透過 Flow 傳遞,但不能因為 API 長得很像,就當成完全一樣的東西。

這部分我覺得值得另外拆一篇。

repeatOnLifecycle 也不是「所有 Flow 都一定 STARTED」

另一個容易變成固定公式的地方是:

repeatOnLifecycle(Lifecycle.State.STARTED)

看久了可能直接變成:

收 Flow 就複製這段。

但真正應該理解的是:

為什麼我要在這個 Lifecycle State 開始這份工作?

對一般 UI State 而言,畫面 STARTED 時開始收集通常很合理。

但還是應該從需求去理解,而不是因為範例都是這樣寫,就認為 STARTED 是 Flow 固定搭配。

這跟昨天 Scope 的問題其實完全一樣。

不是背:

Fragment → viewLifecycleOwner.lifecycleScope

今天也不是背:

Flow → repeatOnLifecycle(STARTED)

而是理解每一層到底在控制什麼。

Scope 和 Lifecycle State 解決的是兩個問題

現在回頭看這段:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(
        Lifecycle.State.STARTED
    ) {
        viewModel.uiState.collect { state ->
            render(state)
        }
    }
}

我會把它拆成三層:

viewLifecycleOwner
↓
這份工作屬於哪個 Lifecycle?

lifecycleScope
↓
這份 Coroutine 最晚活到什麼時候?

repeatOnLifecycle(STARTED)
↓
什麼狀態下才需要執行?

collect
↓
真正要做的工作

這樣原本看起來有點冗長的一段 Code,就開始有理由了。

每一層其實都在回答不同的問題。

Lifecycle-aware 不是「用了 lifecycleScope 就完成了」

昨天整理 Scope 時,我把問題變成:

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

今天再往下一層,其實還要再問:

Owner 還活著的期間,這份工作是不是一直都有執行的必要?

這兩個問題不能混在一起。

Ownership
↓
決定跟誰一起結束

Lifecycle State
↓
決定什麼時候開始或停止工作

所以現在看到:

viewLifecycleOwner.lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        ...
    }
}

我不會再只把它當成一段「官方推薦的 Flow 寫法」。

它其實是在很明確地表達:

這份工作屬於這個 View,而且只有當這個 View 的 Lifecycle 至少處於 STARTED 時,我才需要它。

這樣理解之後,lifecycleScope 和 repeatOnLifecycle 就不是兩個很像、所以不知道為什麼都要寫的 API。

它們只是剛好在回答兩個不同的 Lifecycle 問題。


上一篇
Day 19 - 都是launch,為什麼Coroutine放在哪個Scope差這麼多?
系列文
AI 時代的 Android Engineer:30 天打造我的 AI 開發工作流 共 20 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言