昨天整理到 Process Death 時,提到了 SavedStateHandle。
如果 ViewModel 在 Process 被殺掉之後也會消失,那很直覺就會想到:
是不是重要的 State 都應該放進 SavedStateHandle?
但仔細想又會發現,如果一個畫面的 State 很大,裡面有 API Response、List、Loading、Error、選擇結果,全部保存好像也不太合理。
所以今天想繼續昨天的問題:
Process Death 之後,到底什麼東西需要「保存」,什麼東西其實重新取得就好?
假設有一個搜尋頁面,目前畫面是:
keyword = "Android"
category = "技術書"
sort = "最新"
搜尋結果
↓
50 筆資料
如果 Process 被系統回收,之後使用者回到這個畫面,我希望他看到跟剛才差不多的內容。
第一個直覺可能是:
把整個畫面 State 保存
↓
回來全部還原
但其實不一定需要。
因為這個畫面裡有兩種不同性質的資料:
使用者決定的資料
keyword
category
sort
可以重新取得的資料
搜尋結果
真正無法憑空知道的,是使用者剛才做了哪些選擇。
至於搜尋結果,只要條件還在,就有機會重新 Request。
所以恢復畫面不一定等於:
把畫面所有資料完整保存。
也可能是:
保存足以重新建立畫面的最小資訊。
例如:
class SearchViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
var keyword: String
get() = savedStateHandle["keyword"] ?: ""
set(value) {
savedStateHandle["keyword"] = value
}
}
Process 被重新建立後,我不需要從 SavedStateHandle 拿回完整搜尋結果。
只要拿回:
keyword = Android
再重新:
keyword
↓
Repository
↓
API
↓
Search Result
↓
UI
畫面就有機會重新建立。
所以我現在會把 SavedStateHandle 理解成:
保存「重新建立目前狀態所需要的資訊」,而不是另一個資料庫。
這時就可以再細分。
例如:
搜尋關鍵字
選中的分類
目前選擇的商品 ID
表單填到一半的內容
目前頁面的 Navigation argument
這些資料有一個共同點:
如果它消失,我很難單純從後端或資料庫重新推導回來。
因為它們通常代表使用者目前正在做的事情。
但另外一些 State:
商品名稱
商品圖片
搜尋結果
會員資料
訂單內容
很多原本就有真正的資料來源。
例如:
productId
↓
Repository
↓
API / Database
↓
Product
這時比起保存整個 Product,有時候保存:
productId
反而比較合理。
這件事其實又回到前幾天一直在看的 State Ownership。
假設:
Room
↓
Repository
↓
ViewModel
↓
UI
Room 才是這筆資料真正的來源。
ViewModel 裡的資料只是目前畫面使用的版本。
如果 Process Death:
ViewModel 消失
不代表資料真的消失。
新的 ViewModel 可以:
Repository
↓
Room
↓
重新取得
所以這份資料沒有必要為了 Process Death,再另外完整塞進 SavedStateHandle。
反過來,如果是:
使用者剛剛選了哪個 Tab
這件事情可能沒有任何 Repository 可以重新告訴我。
那它就比較接近需要恢復的 UI State。
所以判斷 State 要不要保存時,我覺得可以多問一題:
這份資料真正的 Source of Truth 在哪裡?
還有第三種。
例如:
isLoading = true
Process Death 之前正在 Loading。
那重新建立之後,我真的需要把:
isLoading = true
恢復嗎?
不一定。
因為新的流程可能是:
重新建立
↓
重新 Request
↓
自然進入 Loading
↓
取得資料
↓
Success
isLoading 本身其實是其他流程產生的結果。
同樣的:
errorMessage
buttonEnabled
formattedPrice
有些 State 都可能是可以重新計算或重新產生的。
如果原始資料還在,就不一定需要把「計算結果」也保存一份。
這讓 State 又可以再拆成:
需要保存的原始資訊
↓
重新取得的資料
↓
可以重新計算的 UI State
假設 Repository 已經有一份商品資料:
Repository
Product(price = 100)
但 SavedStateHandle 又保存:
Product(price = 90)
Process 恢復之後就會出現一個問題:
到底哪一份才是真的?
如果 Server 上的價格已經更新,舊 State 又被完整恢復,就可能把過期資料重新顯示出來。
所以「能不能保存」跟「應不應該保存」其實是不同問題。
有真正資料來源的資料,通常應該回去找真正的資料來源。
而不是讓每一層都保存一份自己的版本。
如果把 Day 15、16、17 放在一起,目前我會先把 State 粗略分成:
1. 需要恢復的 State
例如:
搜尋關鍵字
使用者選擇
輸入中的內容
↓
2. 可以重新取得的資料
例如:
API Response
Room 裡的資料
Repository 管理的資料
↓
3. 可以重新計算的 State
例如:
formattedText
buttonEnabled
部分 Loading 狀態
第一種才比較需要認真考慮:
Process Death 後我要怎麼把它找回來?
第二種要想的是:
真正的資料來源在哪裡?
第三種則要問:
我是不是根本不需要保存它?
昨天講到:
ViewModel
→ 撐過 Configuration Change
SavedStateHandle
→ 幫助 Process Death 後恢復
Room
→ 長期保存
這樣分類雖然方便,但還是太粗。
因為真正做功能時,不是看到:
Process Death
就把 ViewModel 裡所有東西複製到 SavedStateHandle。
更重要的應該是先找:
哪些資訊是使用者剛剛產生的?
↓
哪些資料本來就有 Source of Truth?
↓
哪些 State 可以重新推導?
↓
最後才決定需要保存什麼
所以現在如果要我重新想「畫面狀態恢復」,我會比較傾向這個順序:
State 消失
↓
能重新取得嗎?
├─ Yes → 從 Source of Truth 重新取得
└─ No
↓
能重新計算嗎?
├─ Yes → 重新計算
└─ No
↓
考慮保存恢復所需的最小資訊
這樣看下來,真正需要保存的東西可能比想像中少很多。
狀態恢復的目標,不一定是把死亡前的記憶體完整複製回來,而是保留足夠的資訊,讓畫面能重新建立。
從 Singleton、ViewModel、Process Death 一路看到這裡,我覺得自己其實一直在拆同一件事:
State 不只是「放在哪裡」,還要知道它從哪裡來、誰擁有它,以及消失之後應該保存、重取,還是重新計算。
這幾個問題先想清楚,再決定要不要用 SavedStateHandle,好像就比「重要 State 都存起來」更有方向。