一個 isLoading 布爾值不足以描述真實移動頁面。頁面可能正在刷新,但舊數據仍可讀;也可能請求成功卻為空;斷網時有緩存與沒有緩存,用戶應該看到完全不同的內容。
用數據和同步狀態兩個維度建模
保留 snapshot、lastSyncedAt、requestState、error。渲染規則從這些欄位推導,不把每一種組合都塞進一個巨大的枚舉。這樣「舊數據 + 刷新中」和「舊數據 + 刷新失敗」都能保留頁面主體,同時展示不同提示。
| 快照 | 請求 | 頁面表現 |
|---|---|---|
| 無 | 首次載入 | 骨架屏,保留返回操作 |
| 無 | 失敗 | 錯誤說明與重試入口 |
| 有 | 刷新中 | 繼續展示快照,局部刷新提示 |
| 有 | 失敗 | 展示快照、同步時間和非阻斷錯誤 |
| 空列表 | 成功 | 真正的空態和建立入口 |
把狀態規則放在視圖外
在 RN 的 selector 或 Flutter 的 ViewModel 中實現純函數 deriveScreenState(snapshot, request),讓多個頁面共享規則。進入頁面、下拉刷新、前台恢復、推送消息觸發刷新,都調用同一數據入口;每個入口只改變請求原因,不改變「什麼是可信快照」。
驗收時模擬慢網和離線:連續刷新不會把列表清空;失敗時不會把舊列表誤判為空;賬戶切換後絕不展示上一賬戶的快照。空態只能由一次可信的成功響應確認。
把“空”變成有證據的狀態
以消息列表為例,首次進入頁面時沒有緩存,狀態是 initialLoading;成功返回零條消息後才是 confirmedEmpty。如果先前有二十條消息,用戶下拉刷新時應保持列表可讀,狀態是 refreshingWithData。刷新失敗則進入 staleWithError,繼續保留二十條和上次成功時間。把所有情況壓成 loading | success | error,會迫使界面在刷新期間清空內容,或把失敗誤畫成空列表。
數據倉庫建議保存 {accountId, items, lastSuccessAt, revision, pending},展示狀態由這些事實和當前請求推導,不要讓每個組件各自維護一套布爾值。items=[] 只表示當前數組為空;只有“當前賬號的一次可信成功響應”才能證明真正空態。賬戶切換時先隔離舊賬號快照,再加載新賬號緩存,避免上一賬號的消息在新賬號頁面閃現。
| 事件 | 保留的數據 | 頁面反饋 |
|---|---|---|
| 首次加載失敗 | 無 | 錯誤頁與重試按鈕 |
| 有緩存時刷新失敗 | 上次確認的列表 | 內聯錯誤、同步時間與重試 |
| 請求成功返回空列表 | 當前賬號的空快照 | 明確的空態文案 |
| 賬號切換 | 僅新賬號緩存 | 骨架屏或新賬號快照 |
狀態轉換比截圖更值得測試
以 reducer 或狀態機測試事件序列:成功(20條) → 刷新 → 超時 必須保留二十條;成功(20條) → 成功(0條) 才能變成空態;賬號A加載 → 切換B → A響應 必須丟棄 A 的結果。UI 測試再驗證重試入口、讀屏提示和舊數據標識。這樣測試的是用戶會經歷的變化,而不僅是一張靜態頁面。
