前言
就我這個從iOS跨行過來寫Flutter的人看起來,StatefulWidget 與 StatelessWidget算是我認識Flutter的起點。
Flutter中,所有的UI控件都是由Widget所組成。然後也只分成了這兩種:
- StatelessWidget:在初始化之後,就再也不變的UI控件
- StatefulWidget:內部擁有State,屬性可依事件變化,畫面會隨之更新
StatelessWidget
這個很好理解。
用在一些純展示的頁面、或基礎控件時,直接繼承 StatelessWidget 就能搞定。
它們本身無法隨著 State 變化而重繪,但可以作為大物件中的小零件,跟隨父層重建。
StatefulWidget
我的理解是:State 有點像 MVVM 裡的 View + ViewModel 的混合體。
要刷新畫面,就是在 State 裡呼叫 setState(),它就會執行一次 build(BuildContext context)。
特別要注意:StatefulWidget 只會更新自己和內部的 subWidgets。
有了這兩種 Widget,我們就能開始實戰,刻出完整的 App。
不過隨著頁面複雜度上升,問題開始出現了……
問題一:setState的限制
舉例:我要做一個自製 Radio Button 區塊。
點選一個選項後,希望它高亮,其他選項回到暗沉。
- 如果在 page 的 State 裡 setState(),會整頁重繪,效能浪費
- 如果每顆 Radio Button 都做一個 StatefulWidget,會無法彼此連動
面對這種兩個爛蘋果二選一的情況,我非常痛苦,我需要更細粒度的更新機制
ValueNotifier 與 ValueListenableBuilder
解法之一就是官方的 ValueNotifier(ChangeNotifier 的子類,專門監聽單一值)。就我的理解,ValueNotifier就是個訂閱者模式的實作,它裡頭大概的實作:
class ValueNotifier<T> {
List<VoidCallback?> _listeners = [];
T _value;
T get value => _value;
set value(T newValue) {
if (_value == newValue) return;
_value = newValue;
notifyListeners();
}
void notifyListeners() {
for (var l in _listeners) {
l?.call();
}
}
}
基本上就是綁定Listener完,在每次改動 Value的時候,它就會發通知給所有Listener做事。使用起來大概像這樣:
final ValueNotifier<String?> _selectedRadioId =
ValueNotifier(null);
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<String?>(
valueListenable: _selectedRadioId,
builder: (context, selectedId, child) {
return RadioButton(
isSelected: selectedId == thisRadioId,
selected: () {
_selectedRadioId.value = thisRadioId;
},
);
},
);
}
這樣只要點選這個Radio Button,它就會單獨刷新Listener的區域,也就是這一顆Radio Button,我們完全可以多寫幾個ValueListenableBuilder,就能實作出一套能夠精確更新的區域了。
注意:ValueNotifier 的「等號比對」是用
==;若是class 沒有 override==時比的是記憶體位址。
問題二:資料更新後,跨頁刷新
隨著專案規模成長,跨頁面使用共用資料的需求開始出現
我當然可以直接開個全域變數,然後透過監聽 Router,在Router切換頁面時,即時讀取相關資料進行rebuild,但現在其實有更好的選擇:Provider
它是第三方套件,就是為了解決跨頁更新而生。引入…flutter pub add provider 就行
它基於 Flutter 內建的 InheritedWidget (就是執行期本來就存在,一層層往上疊的Widget樹),把 ChangeNotifier 塞進 widget tree,讓所有子孫 widget 都能存取。
使用上,首先要掛上ChangeNotifierProvider
ChangeNotifierProvider(
create: (_) => MainDataProvider(),
child: MaterialApp(
home: LaunchPage(),
),
);
在Page中使用時
Selector<MainDataProvider, int>(
selector: (context, notifier) => notifier.radioCountChange,
builder: (context, _, child) {
var provider = context.watch<MainDataProvider>();
var radioId = provider.rId;
return RadioButton(
isSelected: radioId == thisRadioId,
selected: () {
provider.rId = thisRadioId;
provider.radioCountChange += 1;
provider.notifyListeners();
},
);
},
);
可以看到,在Widget中存取時,透過Selector,直接就搞定訂閱,取值有兩種方式:
context.watch<T>():有加上listener,值有變會rebuild相關widgetcontext.read<T>():純取值,值改變不會觸發 rebuild
到這基本已解決所有跨頁刷新問題,可以放心使用各種精準刷新、跨頁刷新UI,不必太擔心效能問題。
但,我文章還有最後一個東西推薦…
問題三:超級胖的Provider
當專案愈來愈大,Provider會無可避免的愈長愈大,相關資料愈來愈多。因為它一樣使用notifyListeners()來通知所有訂閱者。想要精準通知使用者,我就想到兩個方法
- 要碼就是區分成多個Provider,然後在好地方將Provider掛上
- 要碼就是幫每個情境加上條件式。然後每個listener在被呼叫時,就會先過一遍條件式,有通過才執行。可以看上面的範例,我就這樣寫
這兩種寫法,都各有難點,第一種在頁面如山似海時很痛苦,同個Class的Provider還只能開一個…
第二種跑起來更是有種浪費的感覺,一堆無效聽眾,看著就覺得浪費效能
最後隆重介紹:Riverpod -> Provider 的進化版
引入方式:flutter pub add flutter_riverpod
同一個作者打造的 Riverpod,就是為了解決 Provider 的痛點
- 不依賴 BuildContext,Provider 可以在全域宣告。
- 自動管理生命週期,沒人用就 dispose。
- 自動依賴收集,只會 rebuild 有使用到的地方。
使用方式:
final radioProvider = StateProvider<String?>((ref) => null);
Consumer(
builder: (context, ref, _) {
final radioId = ref.watch(radioProvider);
return RadioButton(
isSelected: radioId == thisRadioId,
selected: () {
ref.read(radioProvider.notifier).state = thisRadioId;
},
);
},
);
一樣是 ref.watch/ref.read。 就會自動處理是否綁定了
我個人覺得,它的概念就是比Provider更進一步,原本Provider就一定要找個Widget Tree去掛載;Riverpod直接建個樹,寫進全域記憶體中,再把Provider給掛靠上去
我使用起來,可以更彈性的做精細狀態管理,跨頁共用的狀態也更直觀好理解
結語
細數這些狀態管理工具,核心依然就是 StatefulWidget.setState()。
差別只是:
- ValueNotifier 幫忙「局部更新」
- Provider 實作「跨頁共享」
- Riverpod 在Provider的基礎上,更進一步降低對Widget Tree的依賴,並「自動依賴收集與生命週期管理」
對這些工具熟悉,無疑能夠很好的幫助到我們的日常工作,能讓程式執行成本下降、表現提升、並讓程式邏輯集中好管理。
若文中有錯漏,歡迎指正,共勉之









