模組四|狀態管理(Day 15–19)
先備:Day 15–18(四天的對照)。這一篇是模組四的結論,可以單獨讀,但表格裡每一格背後的理由都在前四天。
四天看完三套半(setState、ChangeNotifier/provider、Riverpod、Bloc),該給結論了。先說我最大的體感落差:在 Vue 圈,狀態管理選型根本不是會議題——路徑早就收斂成一條直線;在 Flutter 圈,這是三分天下的路線之爭,接手每個專案都要先考古它信哪一派。這個差異本身就是選型成本,得算進去。這篇不站隊,給的是一棵可辯護的決策樹——而且會說明:如果你跟我一樣用 AI agent 開發,天平的砝碼會移動。
結論先講:
一般專案選 Riverpod,因為它離 Pinia 最近;而我自己也是這樣選的。
前面幾天我埋了三個落差註記——Day 8 說 demo 沒用 provider、Day 10 和 Day 17 說 demo 沒用 Bloc。今天把帳結清。
Vue 的選型是一條升級直線,幾乎沒有分岔:
// 第一站:元件內暫態,ref 就好
const isOpen = ref(false)
// 第二站:邏輯要複用,抽 composable
const { user, loading } = useUser(userId)
// 第三站:跨頁面共享,上 Pinia——到站,沒有第四站
export const useCartStore = defineStore('cart', () => { /* ... */ })
每一站的觸發條件明確(要複用→composable;要共享→Pinia),社群共識高到不需要開會。Vuex 退役後連歷史包袱都清掉了。這種「生態收斂」是 Vue 隱形的生產力來源:新人入職零考古成本,AI 寫 Vue 也幾乎不會選錯工具。
Flutter 官方不欽定方案,只列選項。把四天的對照壓成一張表:
| 需求 | Vue 慣例 | Flutter 對應 | 對照篇 |
|---|---|---|---|
| 元件內暫態 | ref |
setState |
Day 7 |
| 跨元件共享狀態 | Pinia store | Riverpod NotifierProvider |
Day 15–16 |
| 同步衍生值 | computed |
Riverpod Provider |
Day 16 |
| 非同步資料 | composable / useFetch |
FutureProvider + AsyncValue |
Day 16 |
| 嚴格狀態機/審計 | XState(罕用) | Bloc + sealed class | Day 17 |
| 分層依賴下發 | provide/inject | InheritedWidget(及其之上的一切) | Day 18 |
我的決策樹,先寫給人類團隊:
setState 到底,別急著上架構。setState 管純 UI 暫態(展開收合、hover),別把 isExpanded 也塞進全域。再寫給 agent 工作流,兩個砝碼會移動:
最後拿一個具體頁面把樹走一遍:看盤頁,K 線圖、自選股清單、下單面板三塊。K 線圖的縮放平移是純 UI 暫態,setState 關在 widget 裡,誰都不用知道;自選股清單是跨元件共享的即時串流,Riverpod StreamProvider + select 投影,一格報價跳動不牽連整張清單;下單流程碰錢、轉移嚴格(確認→送出→部分成交→撤單)且要留審計軌跡,這是決策樹第 3 條的教科書場景——整個專案唯一的 Bloc,附一段書面理由躺在 README。主幹 Riverpod、例外 Bloc、暫態 setState,三層各司其職:這不是混搭,是分層。
講了四天的選型,最誠實的作法是把自己的答案攤開。Waypoint Air 的 pubspec.yaml 執行時依賴只有五個,跟狀態管理有關的就一行:
dependencies:
flutter_riverpod: ^3.3.2
go_router: ^17.3.0
google_fonts: ^8.2.1
lucide_icons_flutter: ^3.1.15
cupertino_icons: ^1.0.8
沒有 provider、沒有 flutter_bloc、沒有 equatable。也就是說,我在前面幾天教的 provider(Day 8)和 Bloc(Day 10、Day 17),自己一行都沒用在正式的移植上。
這件事值得說清楚,因為它有兩層意思。
第一層是誠實:那三天的 code 都是我為了理解演進脈絡另外寫的最小範例,不是從 repo 挖出來的實戰經驗。你可以用它們建立心智模型,但不要把它們當成「我踩過的坑」——我踩的坑在 Riverpod 這一側。
第二層才是重點:這個選擇本身就是決策樹第 2 條的實證。我是一個人、寫一支中型 side project、從 Vue 端移植過來,三個條件全部指向 Riverpod。而選完之後回頭看,Day 16 那張對照表就是我當初真正用的翻譯字典——defineStore 找 NotifierProvider、computed 找 Provider、useFetch 找 FutureProvider,一格一格對,比讀 Riverpod 官方文件快得多。
至於 Bloc:如果這支 App 真的要收錢、要留審計軌跡,下單那一段我會照決策樹第 3 條走 Bloc。它沒有出現在 pubspec.yaml 裡,不是因為它不好,是因為我的專案還沒有值得它出場的問題。
坑一:三分天下的考古稅。 接手 Flutter 專案,先查 pubspec.yaml 信哪派、哪個版本(Riverpod 1→2、Bloc 7→8 都是破壞性升級)。Vue 人沒繳過這種稅,估工時記得加上去。我的 30 秒判讀順序:見 flutter_bloc + equatable 是 Bloc 派;見 hooks_riverpod 或 riverpod_annotation 是 Riverpod 派(後者代表 codegen 寫法,還要再查 build_runner);只有 provider 的多半是老專案或極簡派,升級路線通常往 Riverpod 走。三個都在?先做好心理準備,再翻 git log 考古是誰在哪一年各自為政。症狀:你照著網路教學寫的 provider 用法貼進專案就報錯,因為這個專案用的是 Riverpod 2 而教學是 3,兩者的 Notifier API 不一樣。
坑二:命名地獄。 provider 是套件、Provider 是 Riverpod 的類別、InheritedWidget 教學也滿口 provide——搜尋錯誤訊息時精確帶上套件名,不然 Stack Overflow 會把你導去隔壁派系的答案。症狀:你搜「flutter provider not found」,前三個結果分別在講三個不同的套件,而它們的解法互相矛盾。
坑三:別混三套。 「登入用 Bloc、購物車用 Riverpod、主題用 provider」的專案真實存在,每一塊都有當初的道理,加起來就是災難。選一套當主幹,例外要有書面理由。症狀:新人問「這個新頁面該用哪一套」,團隊裡三個人給出三個答案,而且三個都舉得出專案裡的先例。
坑四:Web 平台的好消息與壞消息。 好消息:這三套全是純 Dart,在 Flutter Web 上行為與 mobile 一致,選型不用考慮平台。壞消息換個位置收:Vue 選 Pinia 時要想 SSR hydration,Flutter Web 根本沒有 SSR 可想——這個「簡化」的代價,模組六(Day 25 起)談 SEO 時會連本帶利討回來。症狀:這一條現在沒有症狀,症狀會在你被問「為什麼 Google 搜不到我們的產品頁」那天出現。
模組四收官,如果只帶走三件事:一、Riverpod 是 Pinia 人的最短遷移路徑,Day 16 的對照表可以直接當翻譯字典用;二、編譯期窮舉(AsyncValue、sealed class)是 Vue 生態沒有的防線,值得為它重訓肌肉記憶;三、agent 時代的選型天平往「樣板重但檢查嚴」的方案傾斜——樣板的稅由 agent 繳,檢查的紅利由你收。
一句話:一般專案選 Riverpod(離 Pinia 最近)、嚴格流程選 Bloc(把狀態機交給編譯器)、agent 工作流下一致性大於一切。模組四完結,明天 Day 20〈Vite build 流程與最佳化策略回顧〉進入模組五建置與部署——從心智模型走向實測數據,先回顧 Vite 這把量尺。
如果你卡在語法
深入原理