昨天重新看了以前把 Timer 做成 Singleton 的設計,最後其實碰到一個更大的問題:
Android 裡的狀態,到底應該放在哪裡?
如果是 Fragment + ViewModel 的架構,一個很直覺的答案就是 ViewModel。
畫面需要的資料放 ViewModel、Fragment observe,這也是我自己的 Project 很常見的寫法。
但如果真的把所有 State 都往 ViewModel 塞,好像又怪怪的。
所以今天想重新整理的不是「ViewModel 怎麼用」,而是:
ViewModel 到底應該負責保存什麼?
假設直接把資料存在 Fragment:
private var elapsedMillis = 0L
只要 Fragment 被重新建立,原本這個 instance 裡的資料就可能消失。
ViewModel 解決的其中一個重要問題,就是讓 UI 相關資料不要直接綁死在 Activity 或 Fragment instance 上。
Fragment
↓
ViewModel
↓
UI 需要的資料
像螢幕旋轉造成 Activity / Fragment 重建時,同一個 ViewModel 可以繼續存在,新的 UI 再重新取得資料。
所以:
ViewModel 的生命週期比單次 Fragment instance 長。
但這裡有一個很容易產生的誤會:
活得比較久,是不是就代表重要資料都應該放這裡?
其實不是。
例如一個畫面有:
Dialog 是否開啟
目前選到哪個 Tab
Loading 是否顯示
輸入中的內容
這些狀態的共同點是:
它們主要在描述「這個畫面現在長什麼樣子」。
這種資料放在 ViewModel 通常很好理解,尤其當它需要跨過畫面重建,或會影響其他畫面邏輯時。
例如:
使用者點擊設定
↓
ViewModel 更新狀態
↓
Fragment observe
↓
顯示 Dialog
Fragment 不需要自己保存一堆會影響畫面行為的變數。
但也不是任何小狀態都一定要丟進 ViewModel。
如果某個狀態只跟 View 本身短暫的呈現有關,而且消失後重新建立也沒關係,那留在 UI 層可能反而比較簡單。
所以判斷點不是:
這是不是 State?
而是:
這個 State 的生命週期和影響範圍是什麼?
Timer 就開始不太一樣了。
例如:
現在是不是正在計時
目前是 Pomodoro 還是 Stopwatch
開始時間
已經經過多久
這些資料雖然最後會顯示在 UI,但它們並不是「畫面狀態」。
就算現在沒有任何 Fragment 顯示 Timer:
App 到背景
↓
畫面不存在
↓
Timer 還是在進行
所以:
畫面顯示 00:30
和:
Timer 已經經過 30 秒
其實是兩件不同的事情。
前者是 UI 呈現。
後者是功能本身的狀態。
如果把兩者混在一起,就很容易變成 ViewModel 既負責「畫面現在顯示什麼」,又負責「整個 App 現在的 Timer 到底在做什麼」。
這時就要開始問:
如果這個 ViewModel 消失了,這份 State 應不應該一起消失?
如果答案是不應該,那它可能就不該只存在這個 ViewModel。
還有另一種狀態生命週期更長。
例如 Timer 結束後建立的紀錄。
Timer
↓
完成
↓
建立紀錄
↓
寫入 Room
這筆紀錄不只是要跨 Fragment 重建,也不是 App 關掉就可以消失。
它需要跨過:
Fragment 重建
Activity 重建
Process 被殺掉
App 關閉
甚至手機重新開機
這時 ViewModel 當然就不是資料真正應該存在的地方。
所以如果按照「資料應該活多久」來看,可以粗略拆成:
短暫畫面狀態
→ UI / ViewModel
畫面之外仍存在的功能狀態
→ 更長生命週期的持有者
需要長期保存的資料
→ Database / Persistent Storage
這比單純問「要不要放 ViewModel」清楚很多。
這也是我覺得很容易混淆的地方。
ViewModel 可以跨過 Configuration Change,但不代表它可以永遠保存資料。
例如螢幕旋轉:
Activity 被重建
↓
ViewModel 保留
↓
新的 Activity 繼續使用
但如果 App 的 Process 被系統殺掉,記憶體裡的 ViewModel 也會一起消失。
所以:
Configuration Change
≠
Process Death
這兩件事情不能混在一起。
如果某份重要狀態只存在:
ViewModel
↓
MutableLiveData
那它能處理畫面重建,不代表能處理 Process Death。
這也是為什麼 Android 裡還會有 SavedStateHandle、資料庫或其他持久化方式。
它們解決的不是同一層問題。
既然 ViewModel 遇到 Process Death 可能消失,那是不是把所有東西塞進 SavedStateHandle 就好了?
好像又回到同樣的問題。
例如使用者正在填一個搜尋條件,Process 被回收後,希望回來不要全部重填,這種狀態就很適合考慮恢復。
但如果是一整份從 API 拿回來的大量資料,就不一定要完整塞進去。
有時候真正需要保存的是:
query
selectedId
filter
目前操作到哪裡
Process 回來後,再根據這些資訊重新取得資料。
所以「恢復狀態」跟「保存所有資料」也是不同的事情。
如果今天重新設計一個畫面,我覺得自己會先問四個問題:
1. 誰需要這份 State?
2. 它應該活多久?
3. 它消失之後,可不可以重新取得?
4. Process 被殺掉後,需要恢復嗎?
例如:
Dialog 是否開啟
→ 畫面狀態
Timer 是否正在執行
→ 功能狀態
歷史計時紀錄
→ 持久化資料
三個都是 State,但生命週期完全不同。
所以它們也不應該因為:
ViewModel 可以保存 State
就全部被塞進同一個地方。
以前在寫 MVVM 時,很容易形成一條很固定的思路:
UI 有資料
↓
放 ViewModel
↓
LiveData
↓
Fragment observe
這個方向本身沒有問題,但它只回答了「UI 怎麼取得資料」,沒有回答:
這份資料真正屬於誰?
我現在反而覺得這才是比較重要的問題。
ViewModel 很適合站在 UI 和其他層之間,把畫面需要的資料整理成 UI 可以使用的形式,也可以讓畫面相關狀態跨過 Configuration Change。
但它不是因為「比 Fragment 活得久」,就自然變成所有狀態的家。
決定 State 放在哪裡之前,先決定它應該跟誰一起活、跟誰一起消失。
昨天從 Singleton 開始想這件事,今天再看到 ViewModel,其實背後都是同一題:
State Ownership。
誰真正擁有這份狀態,可能比「我要用 LiveData、StateFlow 還是 ViewModel」更值得先想清楚。
下一集見:)