昨天整理 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 應該結束」和「什麼時候這段工作應該執行」是兩個不同的問題。
先看最簡單的:
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。
那就還少了一個條件。
假設 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 的角色就比較清楚了。
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 時
裡面的工作才存在
這樣看就比較不會覺得是在重複做同一件事。
這點也很重要。
假設:
STARTED
↓
開始 collect
STOPPED
↓
collector Cancel
STARTED
↓
建立新的 collector
它不是把原本的 Coroutine 暫停在那裡,回來再從原本那一行繼續。
而是:
離開指定 Lifecycle State 時取消,回來之後重新執行 block。
這也是 repeat 這個名字的意思。
所以放在裡面的工作,應該要能接受:
開始
↓
取消
↓
再次開始
假設 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 時很容易理解的一個地方。
這裡也會順便碰到另一個問題。
假設 Flow 裡不是:
目前畫面 State
而是:
顯示 Toast
Navigation
開啟 Dialog
這些比較像一次性的 Event。
如果 collector 在 STOPPED 時被取消,那這段期間發生的 Event:
回來之後到底應不應該再收到?
答案就不像 State 那麼單純。
例如:
畫面進背景
↓
發出 Navigation Event
↓
沒有 collector
↓
使用者回來
現在應該:
補做 Navigation?
還是:
直接忽略?
這已經不是 repeatOnLifecycle 本身能替我們決定的事情。
因為這是產品行為和資料模型的問題。
所以:
State
和:
Event
雖然都可以透過 Flow 傳遞,但不能因為 API 長得很像,就當成完全一樣的東西。
這部分我覺得值得另外拆一篇。
另一個容易變成固定公式的地方是:
repeatOnLifecycle(Lifecycle.State.STARTED)
看久了可能直接變成:
收 Flow 就複製這段。
但真正應該理解的是:
為什麼我要在這個 Lifecycle State 開始這份工作?
對一般 UI State 而言,畫面 STARTED 時開始收集通常很合理。
但還是應該從需求去理解,而不是因為範例都是這樣寫,就認為 STARTED 是 Flow 固定搭配。
這跟昨天 Scope 的問題其實完全一樣。
不是背:
Fragment → viewLifecycleOwner.lifecycleScope
今天也不是背:
Flow → repeatOnLifecycle(STARTED)
而是理解每一層到底在控制什麼。
現在回頭看這段:
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(
Lifecycle.State.STARTED
) {
viewModel.uiState.collect { state ->
render(state)
}
}
}
我會把它拆成三層:
viewLifecycleOwner
↓
這份工作屬於哪個 Lifecycle?
lifecycleScope
↓
這份 Coroutine 最晚活到什麼時候?
repeatOnLifecycle(STARTED)
↓
什麼狀態下才需要執行?
collect
↓
真正要做的工作
這樣原本看起來有點冗長的一段 Code,就開始有理由了。
每一層其實都在回答不同的問題。
昨天整理 Scope 時,我把問題變成:
這份工作應該跟誰一起活?
今天再往下一層,其實還要再問:
Owner 還活著的期間,這份工作是不是一直都有執行的必要?
這兩個問題不能混在一起。
Ownership
↓
決定跟誰一起結束
Lifecycle State
↓
決定什麼時候開始或停止工作
所以現在看到:
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
...
}
}
我不會再只把它當成一段「官方推薦的 Flow 寫法」。
它其實是在很明確地表達:
這份工作屬於這個 View,而且只有當這個 View 的 Lifecycle 至少處於 STARTED 時,我才需要它。
這樣理解之後,lifecycleScope 和 repeatOnLifecycle 就不是兩個很像、所以不知道為什麼都要寫的 API。
它們只是剛好在回答兩個不同的 Lifecycle 問題。