"State-Management"
- Riverpod 的 reactive 邊界
頁面用了 Riverpod 卻對某些變化沒反應、或 reactive 行為在特定時機炸掉時使用。Riverpod 的 reactive 保證只覆蓋 provider 圖的內部——排查沿著圖的邊界走:變化在圖上嗎、在哪個容器的圖上、節點還活著嗎。
- StreamProvider 包 repository watch stream — broadcast、初始值、dispose 實作點
repository 要補 Stream 觀測出口、接給 Riverpod 消費時使用。訂閱模型選 broadcast 還是單訂閱、新訂閱者拿不拿得到當下狀態、controller 誰負責關——每個問題各有一個會靜默失效的預設答案。
- 手寫 dispose() 沒有呼叫者 — Notifier 的依賴與清理都歸 build() 管
Notifier 用建構子注入依賴、或手寫 dispose() 釋放 Timer 與訂閱時使用。Notifier 的建構與銷毀都由容器管理——UI 不會呼叫 dispose()、掛在方法上的清理等於沒掛;依賴在 build() 內 ref.watch、清理在 ref.onDispose 註冊。
- 加書後統計不刷新 — ref.watch 觀察的是 provider 圖、不是資料庫
頁面用了 Riverpod 卻在資料寫入後不更新、或發現自己在導航返回點補 loadData()、用 EventBus 事件觸發 reload 時使用。ref.watch 的 reactive 範圍是 provider 圖上的狀態變化;資料庫寫入不在圖上,補償刷新的出現就是這個缺口的訊號。
- App 永遠卡在載入畫面 — Riverpod 的 provider 是配方、容器才持有狀態
main() 自建 ProviderContainer 對它觸發初始化、UI 跑在 runApp 的 ProviderScope 裡——兩個容器各持一份 provider 狀態、互不相通,UI 監聽的那份永遠停在初始值。Riverpod 的全域 provider 宣告只是配方、狀態屬於容器實例;跨容器操作是靜默的無效操作。
- await 回來的時候、頁面已經關了 — UnmountedRefException 與 16 個不抽象的檢查點
長 async 流程的每個 await 都是一個 gap:等待期間使用者可能離開、Notifier 被 dispose、回來再寫 state 就炸 UnmountedRefException。修法是每個 gap 後檢查 ref.mounted——而且刻意不抽成 helper:明確的檢查點讓 review 看得見哪個 gap 有守。含「評估必跑、可決定不重構」的技術債處置。
- 只活在結帳流程裡的領域物件 — ephemeral model 與「Rx 外殼、immutable 內核」
流程型狀態(結帳中的輸入金額、支付方式、會員)建模成生命週期等於流程的 ephemeral 物件:結完即丟、下次全新,殘留狀態忘記重置的 bug 被結構性消滅。實作形態是 reactive 外殼包 immutable 內核——對外只開語意化變更方法、每次變更是原子的狀態替換。
- 會員身分、計價、支付方式必須一起換 — 耦合欄位的原子切換
多個狀態欄位被同一條業務規則綁住時,分開的 setter 會製造不一致的中間態;把切換收成單一方法、一次狀態更新內同步全部欄位,並注意衍生值重算的順序。以 POS 結帳的會員登出重算為例,含不變式收進 model 的 canCheckout 設計。
- 新增欄位忘記同步 reset — 跨測試狀態洩漏的系統性根因
測試結果取決於執行順序、看似功能 bug 實為上一個 test case 狀態沒清乾淨。根因是新增 private 欄位時沒同步更新 reset,隱含契約沒被顯性化。