最近又有人在群組問 Flutter 狀態管理...嗯,這問題大概跟「午餐吃什麼」一樣,真的是萬年經典。好像每個剛入門 Flutter 的人,都會在這裡卡關。Provider、BLoC、GetX... 或是乾脆 `setState` 一路寫到爆?感覺好像選錯了,整個專案就會走上歪路一樣。😅
老實說,我自己也為這件事糾結了很久,特別是之前為了準備一個線上的分享會,把這幾個主流的方案都重新玩了一遍。今天就來聊聊我的一些心得吧,不算是教學,比較像是一個...嗯...過來人的筆記。
重點一句話
如果你很急,那我先說結論:先挑一個你看得順眼、能理解的方案用熟,真的比你花三個月研究哪個是「全世界最好的」還重要。真的,寫就對了。
怎麼做:從魔法到工廠,四種狀態管理的心路歷程
在深入聊之前,有個觀念要先有... 就是 Ephemeral State (短暫狀態) 跟 App State (應用程式狀態)。
簡單講,一個按鈕按下去會不會轉圈圈、一個輸入框現在有沒有內容,這種只跟單一畫面、單一 Widget 有關的,就是 Ephemeral State。這種東西用最簡單的 `setState` 就好,真的不用想太多。
但如果是像購物車裡的商品、使用者登入後的個人資料... 這種需要在 A 畫面修改、B 畫面也要能看到的資料,這就是 App State。我們今天吵半天的,主要就是怎麼管理這個 App State。
1. setState(): 最初的感動與最後的眼淚
我猜每個人剛學 Flutter,第一個學會讓畫面動起來的方法,就是 `setState()` 吧。在 `StatefulWidget` 裡面,呼叫一下這個函式,然後... 哇!畫面就更新了!跟變魔術一樣。 ✨
它的邏輯超級單純:
// 某個 StatefulWidget 裡面
bool _isLoading = false;
List<Recipe> _recipes = [];
void fetchRecipes() {
setState(() {
_isLoading = true;
});
// ... 假裝這裡去打了 API ...
// API 回來後...
setState(() {
_recipes = newRecipesFromApi;
_isLoading = false;
});
}
基本上就是宣告幾個變數來代表 UI 的狀態 (載入中、資料列表...),然後透過 `setState` 來通知 Flutter:「嘿!我這裡有東西變了,麻煩你重畫一下喔!」
這東西在寫一些小 demo、小玩具的時候真的超好用。但... 當你的 App 開始變大,比如一個頁面有十幾個地方需要依據不同狀態來改變時,你就會發現你的 `setState` 散落在各處,然後你會開始搞不清楚到底是哪個 `setState` 觸發了哪個 UI 的更新。最後,為了改一個小東西,你可能要 `Ctrl+F` 找半天。這就是所謂的... 技術債吧。
2. Provider: 貼在門口的公告欄
當你開始抱怨 `setState` 難管理的時候,就會有前輩跟你說:「用 Provider 啦,入門首選!」
Provider 的概念,我自己是覺得,很像一個公告欄。它用了 `ChangeNotifier`、`ChangeNotifierProvider` 跟 `Consumer` 這三個核心的東西。
- ChangeNotifier: 這就是你的「狀態本人」,或者說是那張「公告」。它負責保管資料,比如食譜列表。當資料變動時,它會大喊一聲 `notifyListeners()`,通知大家來看喔!
- ChangeNotifierProvider: 這就是「公告欄」。你把它放在你 App 的某個高層級的地方,像是整個頁面的最外層。這樣一來,所有在這個公告欄底下的 Widget,都能看到這張公告。
- Consumer: 這就是關心這張公告的「村民」。它會包住你想更新的 Widget,然後一直聽著公告欄有沒有新消息。一旦公告更新了,它就會立刻重畫自己包住的那塊 UI。
說起來有點抽象,但概念上就是把「狀態」從 UI Widget 自己身上抽離出來,集中管理。這樣一來,你的 UI 就單純負責顯示,而資料邏輯就交給 Provider 去處理。分工明確很多。
老實說,Flutter 官方文件之前也是首推這個,你可以去看一下他們的說明。不過呢,我自己覺得,像台灣有些 Flutter 開發社群或是一些大神分享的,他們會提到 Provider 雖然好上手,但當邏輯一複雜,比如一個動作要同時改變好幾個狀態時,`ChangeNotifier` 就會開始變得有點臃腫。這點在實作上要稍微注意一下。
3. BLoC: 蓋一座自己的SOP工廠
然後,當你的 App 變得更大,團隊有好幾個人,你會開始聽到 BLoC (Business Logic Component) 這個詞。而且通常推崇 BLoC 的人,都會散發出一種「這才是正道」的氣場。😂
BLoC 的學習曲線... 嗯... 真的比較陡。它的核心精神是「UI 跟邏輯完全隔離」,而且是透過 `Events` (事件) 和 `States` (狀態) 來溝通。
它的運作模式很像一個有嚴格 SOP 的工廠:
- UI:絕對不自己處理邏輯。它只會做一件事:發送「訂單 (Event)」。比如「使用者按了重新整理按鈕」,它就發一個 `RefreshEvent` 的訂單給工廠。
- Bloc:這就是工廠本人。它接收到訂單 (Event) 後,會根據訂單類型,開始跑內部的標準作業流程。可能會去打 API、算數學...等等。
- State:工廠作業完成後,不會直接跟 UI 溝通,而是會產出一個「成品 (State)」。比如 `LoadingState` (處理中)、`SuccessState` (成功,附帶產品資料)、`ErrorState` (失敗,附帶錯誤訊息)。
- UI:UI 則是隨時在監看工廠的出口,看到新的成品 (State) 出來了,就知道該怎麼更新畫面。看到 `LoadingState` 就顯示轉圈圈,看到 `SuccessState` 就顯示資料。
好處是什麼?超級清楚。UI 就是 UI,邏輯就是邏輯。而且因為輸入 (Event) 和輸出 (State) 都被定義得一清二楚,所以非常好寫「單元測試」。這在大型專案和團隊協作中,真的是救命丹。你可以確保你的工廠在收到特定訂單時,一定會產出預期的成品。
但壞處... 就是文章開頭說的。你可能只是想熱個便當,但 BLoC 會要你先蓋一座食品加工廠。那個所謂的 "Boilerplate code" (樣板程式碼) 真的不是開玩笑的,為了實作一個簡單功能,你可能要先定義好幾個 Event 類別、State 類別,然後再寫 Bloc 本體... 有點累。
4. GetX: 一把方便過頭的瑞士刀
最後是 GetX。如果說 BLoC 是正規的重工業,那 GetX 就像一把萬能的瑞士刀,或者... 黑魔法?🤔
GetX 的目標就是「快」。它把狀態管理、路由管理 (跳頁)、依賴注入 (DI) 全都包在一起了。寫起來的程式碼量真的可以少得驚人。
// Controller 裡面
var isLoading = true.obs; // .obs 讓它變成可監聽的
var recipeList = <Recipe>[].obs;
// UI 裡面
Obx(() => controller.isLoading.value
? CircularProgressIndicator()
: ListView(...)
);
你看,用一個 `.obs` 把變數變成「可觀察的」,然後在 UI 用 `Obx` 包起來,就搞定了。沒有 `Provider`、沒有 `Consumer`、沒有 `Event`、沒有 `State`... 什麼都沒有,但它就是會動。
這很誘人,對吧?尤其是對想快速開發、做 MVP (最小可行性產品) 的人來說。但...天下沒有白吃的午餐。GetX 的方便來自於它高度的封裝,很多東西它都幫你「在背後」做掉了。這也代表當事情出錯時,你很難知道問題出在哪一環。而且 GetX 的整個生態系比較圍繞著它的創作者,不像 Provider 或 BLoC 是由更廣泛的社群在維護,這也是一個隱性的風險。
所以到底要用哪個?一張表讓你感受一下
說了這麼多,我知道你可能還是很亂。我整理了一個我自己主觀感受的比較表,你參考看看。這不是什麼官方標準答案,比較像是我自己的吐槽大會。😂
| 評估項目 | setState | Provider | BLoC | GetX |
|---|---|---|---|---|
| 上手難度 | ⭐ 根本不用學,內建的。 |
⭐⭐ 花個下午看看文件,大概就懂了。 |
⭐⭐⭐⭐ 可能要花幾天,而且要轉變一下思考模式。 |
⭐.5 API 超簡單,但要理解它「為什麼能動」可能要更久。 |
| 程式碼量 | 看情況 邏輯跟 UI 混一起,可多可少。 |
中等 比 setState 多一點,但還算直觀。 |
非常多 傳說中的 Boilerplate 大戶,做好心理準備。 |
非常少 少到你會懷疑人生。 |
| 適合場景 | 個人小玩具、單一頁面的小互動。 | 中小型專案,或是當作新手的起點。很穩。 | 中大型、需要長期維護、多人協作的專案。 | 快速開發 MVP、個人專案、或是你不怕黑魔法的話。 |
| 我個人吐槽 | 是初戀,但通常不會是最後的歸宿。 | 四平八穩的好好先生,不出錯但也不驚艷。 | 嚴謹的德國工程師,很可靠但有點囉嗦。 | 聰明但有點獨裁的天才,跟他混很爽,但哪天他消失你就慘了。 |
反例與誤解釐清
我覺得新手最大的誤解,就是以為「我必須為我的專案選擇一個『最好』的狀態管理方案,然後從一而終」。
但現實世界根本不是這樣。其實... 你可以混用。
這聽起來很像異端,但你想想,一個 App 裡面,本來就有不同複雜度的頁面啊。比如「關於我們」頁面,可能根本沒有任何動態資料,那連 `setState` 都不用。一個「設定」頁面,可能只有幾個開關,用 `setState` 或許就綽綽有餘。而最複雜的「主頁」或「購物車」,你再用 Provider 或 BLoC 去處理。
沒有規定說整個 App 只能有一種模式。重點是你的團隊要有共識,知道「哪種情境下,我們優先用哪種方法」,這樣才不會亂掉。
與其追求一個完美的、適用於所有場景的「銀彈」,不如建立一個務實的、有彈性的「工具箱」。
總結一下... 唉,其實也沒什麼好總結的。狀態管理就是這樣,一個坑接著一個坑。我自己的路徑是 `setState` -> `Provider` -> `BLoC`,GetX 我只有在小專案玩過。我覺得這個路徑對建立觀念蠻有幫助的。
你會先體會到 `setState` 的痛,然後才會理解 Provider 把邏輯抽離出來的好。接著當 App 變更大,你會開始需要 BLoC 帶來的紀律和可測試性。
所以,如果你是新手,不要怕。從 Provider 開始吧,它是一個很棒的起點。等到你覺得它不夠用了,你自然會知道該往 BLoC 或其他方案前進了。
好了,大概就這樣吧。希望這些廢話對你有幫助。你現在是用哪個方案?或是有沒有踩過什麼特別好笑的雷?留言分享一下吧!搞不好你的痛點我剛好也遇過 😂
