嗯...今天要來聊聊 Flutter 的狀態管理。老實說,這大概是所有新手,甚至是寫了一陣子的人,都會覺得有點頭痛的地方。到底什麼是「狀態」?為什麼要「管理」它?
我自己的理解啦,很簡單。想像一下,你在 App 裡按了一個「讚」,那個愛心從空心變成實心,這個「實心」的樣子就是一個狀態。你把商品加入購物車,購物車圖示右上角跳出一個數字「1」,這個「1」也是一個狀態。基本上,所有會變動的資料、所有會影響畫面長相的資訊,都是狀態。
那...管理就很重要了。如果你的 App 很小,可能沒什麼感覺。但當 App 變大、變複雜,這個狀態可能在 A 頁面被改變,但 B、C、D 頁面都需要知道它變了,然後更新自己的畫面。這時候,你如果沒有一套好的方法,就會開始到處亂傳資料,程式碼會變得像一團打結的毛線,超可怕。
所以,Flutter 狀態管理到底在煩什麼?
煩的點就在於,Flutter 整個架構都是 Widget。一切都是 Widget。你的按鈕是 Widget,你的文字是 Widget,連整個頁面佈局都是一堆 Widget 疊起來的。這就帶來一個問題:A Widget 的狀態,要怎麼「通知」遠在天邊的 B Widget?
Flutter 原生提供的方法,像是把 state 往上層提,然後透過建構子(constructor)一層一層往下傳...嗯,頁面一多,傳到後來真的會懷疑人生。這就是所謂的 "prop drilling"。真的很痛苦,相信我。
所以社群才發明了各種解決方案,像 BLoC、Redux、MobX,還有今天要談的 Provider。對了,現在還有 Riverpod。選擇真的很多,多到會讓人選擇困難。
先說結論:那用 Provider 好嗎?
我自己是覺得,如果你是初學者,或是你的專案規模沒有到非常巨大,Provider 真的很夠用了。它...怎麼說呢,它就是 Google 官方曾經推薦過的一個方案(雖然現在他們更推 Riverpod),它夠簡單、夠直覺。它讓你用一個相對文明的方式,去處理狀態的傳遞跟更新,而不用寫一堆噁心的樣板程式碼(boilerplate code)。
它的核心概念就是把「狀態」跟「UI」分離。讓管資料的專心管資料,畫面的專心畫畫面。資料一變,就通知有在聽的畫面們:「欸,我變了喔,你各位該更新了!」就這樣。
好,那 Provider 到底怎麼用?
理論講再多都沒用,直接看程式碼最快。我們就用最經典的「計數器」範例來講。老掉牙,但真的最清楚。
首先,你得在你的 `pubspec.yaml` 檔案裡加上 provider。版本號...就用新一點的吧。
dependencies:
flutter:
sdk: flutter
provider: ^6.1.2 # 或是更新的版本
然後,我們要建立一個專門管「計數器狀態」的 class。我自己習慣叫它 Model 或 Notifier。
import 'package:flutter/material.dart';
// 這是一個專門管理計數器狀態的 class
class CounterModel with ChangeNotifier {
int _count = 0;
// 給外面讀取用的
int get count => _count;
// 加一的方法
void increment() {
_count++;
notifyListeners(); // 超級重要!通知所有在聽的 Widget 更新
}
// 減一的方法
void decrement() {
_count--;
notifyListeners(); // 一樣,要通知
}
}
這裡面最重要的就是 `ChangeNotifier` 跟 `notifyListeners()`。`ChangeNotifier` 是一個 mixin,它給了你的 class 一種「廣播能力」。當你呼叫 `notifyListeners()` 的時候,它就會對所有「訂閱」它的 Widget 大喊:「我更新了!」
接著,要把這個 `CounterModel` 放到你的 Widget Tree 的最上層,或是你需要用到它的那個分支的最上層。這樣它底下的所有子孫 Widget 才能存取到它。
import 'package:provider/provider.dart';
void main() {
runApp(
ChangeNotifierProvider(
create: (context) => CounterModel(), // 在這裡建立實例
child: const MyApp(),
),
);
}
`ChangeNotifierProvider` 的作用就像是在你的 App 的根部,放了一個看不見的狀態管理員。
實作指引:幾個關鍵點
有了 Provider,那 UI 怎麼讀取跟修改狀態?主要有兩個方法:`Consumer` 和 `Provider.of`。這兩個的選擇,對效能影響很大。
我們直接看 UI 怎麼寫:
class CounterScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Provider 計數器')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('目前的數字是:'),
// 重點1: 用 Consumer 包住需要更新的 Widget
Consumer<CounterModel>(
builder: (context, counter, child) {
return Text(
'${counter.count}',
style: Theme.of(context).textTheme.headlineMedium,
);
},
),
],
),
),
floatingActionButton: FloatingActionButton(
// 重點2: 觸發事件時,用 Provider.of 呼叫方法
onPressed: () {
Provider.of<CounterModel>(context, listen: false).increment();
},
child: Icon(Icons.add),
),
);
}
}
看到了嗎?顯示數字的 `Text` Widget 被 `Consumer` 包起來了。這代表,只有這個 `Text` Widget 會在 `notifyListeners()` 被呼叫時,重新 build。整個 `CounterScreen` 的其他部分,像是 `AppBar` 或 `Column`,都不會動。這就是 Provider 在效能上的優勢。
而那個按鈕 `FloatingActionButton`,它只是要去呼叫 `increment()` 方法,它自己不需要因為數字改變而重畫。所以我們用 `Provider.of
我自己覺得最重要的一點:listen: false
真的,`listen: false` 是剛學 Provider 最容易忘記,但也最關鍵的東西。如果你在一個只是要「觸發方法」,而不需要「監聽數值變化」的地方,忘了加 `listen: false`,會發生什麼事?
那整個 `build` 方法都會被註冊為監聽者。也就是說,一旦 `notifyListeners()` 被呼叫,你整個頁面都會跟著重畫一次。在計數器這種小 App 裡,你可能沒感覺。但如果你的頁面很複雜,有很多網路請求、動畫...那每次更新數字,整個頁面都重算一次,你的 App 就會開始卡頓,甚至閃退。
所以,我整理了一下這幾個常用東西的用法時機:
| 用法 | 使用時機 | 個人筆記 |
|---|---|---|
Consumer |
當你只需要在 UI 的一小塊地方顯示狀態,並且希望它自動更新時。 | 這應該是你的首選。可以把更新範圍縮到最小,效能最好。寫法稍微囉嗦一點,要多一層 builder。 |
Provider.of |
當你需要在 `build` 方法中取得狀態值,而且...嗯...整個 `build` 方法都跟這個狀態有關。 | 老實說,我現在比較少直接這樣用。除非是很簡單的 Widget,不然很容易不小心讓整個 Widget 都重新 build。 |
Provider.of |
在按鈕的 `onPressed` 或其他事件回呼(callback)中,你只是想呼叫 Model 裡的方法,不想監聽變化。 | 這個超級常用!忘記加 `listen: false` 是效能殺手。把它當成一個習慣吧。 |
Selector |
當你的 Model 裡有很多狀態,但這個 Widget 只關心其中「一個」狀態的變化時。 | 比 `Consumer` 更精準。例如 Model 有 a, b, c 三個值,但這個 UI 只需要在 a 變的時候重畫,就可以用 `Selector`。算是進階版的 `Consumer`。 |
Provider 的極限在哪?什麼時候該換?
Provider 很棒,但它不是萬能的。它最大的問題,我覺得,是它依賴 `BuildContext`。你必須要把 `ChangeNotifierProvider` 放在某個 Widget 之上,底下的 Widget 才能用 `context` 去找到它。
這會造成幾個問題:
- Provider not found exception:新手最常遇到的錯誤。就是你在一個還沒有 Provider 的 `context` 底下,就想去讀取它。
- 多個 Provider 組合不易:當你有好幾個狀態要管理,你就得寫一堆 `MultiProvider`,像這樣一層包一層,看起來有點...醜。
- 狀態之間的依賴:如果 A Provider 的狀態,需要讀取 B Provider 的狀態來計算,寫起來會有點彆扭。
這也是為什麼 Provider 的作者 Remi Rousselet 後來又開發了 Riverpod。Riverpod 可以說是 Provider 的進化版,它不依賴 `BuildContext`,是 Compile-safe 的,也更容易做狀態之間的依賴管理。
說到這個,我就觀察到一個現象。在國外的 Flutter 社群或官方文件(像是 Flutter official documentation)裡,他們很早就開始推廣大家從 Provider 遷移到 Riverpod。但在台灣的一些社群,例如我常逛的 Flutter Taiwan 臉書社團,我還是看到很多新手教學或專案分享,是以 Provider 為主的。這沒有對錯,只是反映出 Provider 的學習曲線確實比較平緩,對入門者來說,還是非常有吸引力的第一站。
所以我的建議是,如果你是初學者,先學 Provider 絕對沒錯。先把狀態分離、單向數據流這些觀念搞懂。等到你覺得 Provider 開始讓你綁手綁腳,再來學 Riverpod,你會覺得豁然開朗。
反例與誤解釐清
最後,講幾個我常看到大家對 Provider 的誤解。
- 誤解一:「Provider 就是 ChangeNotifier。」
不完全是。Provider 是一個依賴注入(Dependency Injection)的框架,`ChangeNotifierProvider` 只是它其中一種用法,用來提供可以「通知變化」的物件。你也可以用 Provider 來提供一般的物件,那些物件不會通知更新。 - 誤解二:「所有狀態都要放進一個巨大的 Model 裡。」
千萬不要。好的做法是根據功能切分。例如,你有「使用者認證」跟「購物車」兩個功能,你應該建立 `AuthModel` 和 `CartModel` 兩個獨立的 Provider,而不是把使用者資料跟購物車商品全塞在一起。 - 誤解三:「用了 Provider,就不能用 setState 了。」
當然可以。如果一個狀態只在某個單一的 Widget 內部使用,完全不會影響到其他 Widget(例如:一個動畫的 Controller、一個輸入框的文字),那用 `StatefulWidget` 跟 `setState` 反而是最簡單、最有效率的做法。不要為了用 Provider 而用 Provider。
大致上是這樣吧。Provider 是個很好的起點,它能讓你用比較正規的方式來組織你的 Flutter App。但它也只是一個工具,最重要的還是背後「狀態與 UI 分離」的思想。搞懂了這個,不管用哪個工具,都會順手很多。
聊了這麼多,換你分享看看:
你剛開始學 Flutter 時,第一個卡關的狀態管理工具是什麼?是 Provider 嗎?還是直接挑戰了 BLoC 或 Riverpod?在下面留言分享你的經驗吧!
