[Flutter 新手到進階],從 setState 到 Riverpod:理解 Flutter 狀態更新機制

更新 發佈閱讀 11 分鐘


前言

就我這個從iOS跨行過來寫Flutter的人看起來,StatefulWidget 與 StatelessWidget算是我認識Flutter的起點。
Flutter中,所有的UI控件都是由Widget所組成。然後也只分成了這兩種:

  1. StatelessWidget:在初始化之後,就再也不變的UI控件
  2. 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

解法之一就是官方的 ValueNotifierChangeNotifier 的子類,專門監聽單一值)。就我的理解,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相關widget
  • context.read<T>():純取值,值改變不會觸發 rebuild

到這基本已解決所有跨頁刷新問題,可以放心使用各種精準刷新、跨頁刷新UI,不必太擔心效能問題。
但,我文章還有最後一個東西推薦…


問題三:超級胖的Provider

當專案愈來愈大,Provider會無可避免的愈長愈大,相關資料愈來愈多。因為它一樣使用notifyListeners()來通知所有訂閱者。想要精準通知使用者,我就想到兩個方法

  1. 要碼就是區分成多個Provider,然後在好地方將Provider掛上
  2. 要碼就是幫每個情境加上條件式。然後每個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的依賴,並「自動依賴收集與生命週期管理」

對這些工具熟悉,無疑能夠很好的幫助到我們的日常工作,能讓程式執行成本下降、表現提升、並讓程式邏輯集中好管理。

若文中有錯漏,歡迎指正,共勉之


留言
avatar-img
留言分享你的想法!
avatar-img
青石觀點
0會員
5內容數
各式心得分享
你可能也想看
Thumbnail
雙11於許多人而言,不只是單純的折扣狂歡,更是行事曆裡預定的,對美好生活的憧憬。 錢錢沒有不見,它變成了快樂,跟讓臥房、辦公桌、每天早晨的咖啡香升級的樣子! 這次格編突擊辦公室,也邀請 vocus「野格團」創作者分享掀開蝦皮購物車的簾幕,「加入購物車」的瞬間,藏著哪些靈感,或是對美好生活的想像?
Thumbnail
雙11於許多人而言,不只是單純的折扣狂歡,更是行事曆裡預定的,對美好生活的憧憬。 錢錢沒有不見,它變成了快樂,跟讓臥房、辦公桌、每天早晨的咖啡香升級的樣子! 這次格編突擊辦公室,也邀請 vocus「野格團」創作者分享掀開蝦皮購物車的簾幕,「加入購物車」的瞬間,藏著哪些靈感,或是對美好生活的想像?
Thumbnail
In today's fast-paced and often stressful world, finding moments of calm and tranquility is essential. Meditation apps have become valuable tools for
Thumbnail
In today's fast-paced and often stressful world, finding moments of calm and tranquility is essential. Meditation apps have become valuable tools for
Thumbnail
本文介紹了在網站開發中如何運用狀態機的原則和設計方法。通過具體案例分析,以及狀態和數據的區分,詳細介紹了狀態機的設計原則和應用。讀者可以通過本文瞭解如何將狀態機應用於實際的網站開發中。
Thumbnail
本文介紹了在網站開發中如何運用狀態機的原則和設計方法。通過具體案例分析,以及狀態和數據的區分,詳細介紹了狀態機的設計原則和應用。讀者可以通過本文瞭解如何將狀態機應用於實際的網站開發中。
Thumbnail
# 簡介 身為一位專注於 Vue.js 的前端開發者,這是我第一次嘗試構建 Flutter 網頁應用。讓我們開始吧! ## 第一次嘗試 ### 第一步:創建一個 Flutter 應用 首先,通過運行以下命令來創建一個新的 Flutter 項目: ```sh flutter
Thumbnail
# 簡介 身為一位專注於 Vue.js 的前端開發者,這是我第一次嘗試構建 Flutter 網頁應用。讓我們開始吧! ## 第一次嘗試 ### 第一步:創建一個 Flutter 應用 首先,通過運行以下命令來創建一個新的 Flutter 項目: ```sh flutter
Thumbnail
本篇文章將分享手機App設計教學,並往後介紹使用Flutter開發App的相關知識和技巧。透過這系列的分享,讀者將能夠學習如何利用設計和程式開發技能來製作一個App。文章中也提供了一些靈感來源和教學資源,幫助讀者進行設計和開發的思考和學習。
Thumbnail
本篇文章將分享手機App設計教學,並往後介紹使用Flutter開發App的相關知識和技巧。透過這系列的分享,讀者將能夠學習如何利用設計和程式開發技能來製作一個App。文章中也提供了一些靈感來源和教學資源,幫助讀者進行設計和開發的思考和學習。
Thumbnail
ListViewItem 交換順序以及拖曳效果。
Thumbnail
ListViewItem 交換順序以及拖曳效果。
Thumbnail
有沒有想過,即使沒有任何編程背景,你的創意也能在六個月內轉化成真實的App?我可以以自身經歷跟你說有了 No-Code Tool (無代碼工具) 和 AI 的幫助,這一切都是可能的!你一行 code 都不需要打,甚至也無須學習任何編程語言!沒有什麼比實踐一個自小認為不可能的任務還振奮人心的事了!
Thumbnail
有沒有想過,即使沒有任何編程背景,你的創意也能在六個月內轉化成真實的App?我可以以自身經歷跟你說有了 No-Code Tool (無代碼工具) 和 AI 的幫助,這一切都是可能的!你一行 code 都不需要打,甚至也無須學習任何編程語言!沒有什麼比實踐一個自小認為不可能的任務還振奮人心的事了!
Thumbnail
根據初學者設計了 Kotlin 程式語言的基礎課程,從 Android Studio 到 Android App 開發,提供完整指引。由基礎開始,傳授開發技巧。課程分為三部分:環境安裝、常用元件與界面設計,以及高階技巧如 DataStore、Room 資料儲存與網路處理。
Thumbnail
根據初學者設計了 Kotlin 程式語言的基礎課程,從 Android Studio 到 Android App 開發,提供完整指引。由基礎開始,傳授開發技巧。課程分為三部分:環境安裝、常用元件與界面設計,以及高階技巧如 DataStore、Room 資料儲存與網路處理。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News