[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
工程師Tim的迭代日誌:如何活得更健康、更有錢、更有趣
0會員
5內容數
我是 Tim,一名享受運動的資深開發者。在程式開發、家庭育兒與投資理財之間,我利用長期主義策略,實踐「健康、財富、有趣」的平衡。這是我中年進化的實驗日誌,邀請你一起持續微調人生
你可能也想看
Thumbnail
本篇文章將分享手機App設計教學,並往後介紹使用Flutter開發App的相關知識和技巧。透過這系列的分享,讀者將能夠學習如何利用設計和程式開發技能來製作一個App。文章中也提供了一些靈感來源和教學資源,幫助讀者進行設計和開發的思考和學習。
Thumbnail
本篇文章將分享手機App設計教學,並往後介紹使用Flutter開發App的相關知識和技巧。透過這系列的分享,讀者將能夠學習如何利用設計和程式開發技能來製作一個App。文章中也提供了一些靈感來源和教學資源,幫助讀者進行設計和開發的思考和學習。
Thumbnail
全新版本的《三便士歌劇》如何不落入「復刻經典」的巢臼,反而利用華麗的秀場視覺,引導觀眾在晚期資本主義的消費愉悅之中,而能驚覺「批判」本身亦可能被收編——而當絞繩升起,這場關於如何生存的黑色遊戲,又將帶領新時代的我們走向何種後現代的自我解構?
Thumbnail
全新版本的《三便士歌劇》如何不落入「復刻經典」的巢臼,反而利用華麗的秀場視覺,引導觀眾在晚期資本主義的消費愉悅之中,而能驚覺「批判」本身亦可能被收編——而當絞繩升起,這場關於如何生存的黑色遊戲,又將帶領新時代的我們走向何種後現代的自我解構?
Thumbnail
Notion 是一個高度自由的筆記式平台,讓你可以根據自己的需求定製各種功能。在今天的教學中,我將向你介紹如何透過 WidgetStore 將 Notion 帶入一個全新的層次,並推薦三種在其他外掛中需要付費版才能使用的功能,以便你在學習和工作中發揮更大的效益。讓我們看下去!
Thumbnail
Notion 是一個高度自由的筆記式平台,讓你可以根據自己的需求定製各種功能。在今天的教學中,我將向你介紹如何透過 WidgetStore 將 Notion 帶入一個全新的層次,並推薦三種在其他外掛中需要付費版才能使用的功能,以便你在學習和工作中發揮更大的效益。讓我們看下去!
Thumbnail
根據初學者設計了 Kotlin 程式語言的基礎課程,從 Android Studio 到 Android App 開發,提供完整指引。由基礎開始,傳授開發技巧。課程分為三部分:環境安裝、常用元件與界面設計,以及高階技巧如 DataStore、Room 資料儲存與網路處理。
Thumbnail
根據初學者設計了 Kotlin 程式語言的基礎課程,從 Android Studio 到 Android App 開發,提供完整指引。由基礎開始,傳授開發技巧。課程分為三部分:環境安裝、常用元件與界面設計,以及高階技巧如 DataStore、Room 資料儲存與網路處理。
Thumbnail
本文介紹了在網站開發中如何運用狀態機的原則和設計方法。通過具體案例分析,以及狀態和數據的區分,詳細介紹了狀態機的設計原則和應用。讀者可以通過本文瞭解如何將狀態機應用於實際的網站開發中。
Thumbnail
本文介紹了在網站開發中如何運用狀態機的原則和設計方法。通過具體案例分析,以及狀態和數據的區分,詳細介紹了狀態機的設計原則和應用。讀者可以通過本文瞭解如何將狀態機應用於實際的網站開發中。
Thumbnail
本文深度解析賽勒布倫尼科夫的舞臺作品《傳奇:帕拉贊諾夫的十段殘篇》,如何以十段殘篇,結合帕拉贊諾夫的電影美學、象徵意象與當代政治流亡抗爭,探討藝術在儀式消失的現代社會如何承接意義,並展現不羈的自由靈魂。
Thumbnail
本文深度解析賽勒布倫尼科夫的舞臺作品《傳奇:帕拉贊諾夫的十段殘篇》,如何以十段殘篇,結合帕拉贊諾夫的電影美學、象徵意象與當代政治流亡抗爭,探討藝術在儀式消失的現代社會如何承接意義,並展現不羈的自由靈魂。
Thumbnail
有沒有想過,即使沒有任何編程背景,你的創意也能在六個月內轉化成真實的App?我可以以自身經歷跟你說有了 No-Code Tool (無代碼工具) 和 AI 的幫助,這一切都是可能的!你一行 code 都不需要打,甚至也無須學習任何編程語言!沒有什麼比實踐一個自小認為不可能的任務還振奮人心的事了!
Thumbnail
有沒有想過,即使沒有任何編程背景,你的創意也能在六個月內轉化成真實的App?我可以以自身經歷跟你說有了 No-Code Tool (無代碼工具) 和 AI 的幫助,這一切都是可能的!你一行 code 都不需要打,甚至也無須學習任何編程語言!沒有什麼比實踐一個自小認為不可能的任務還振奮人心的事了!
Thumbnail
若說易卜生的《玩偶之家》為 19 世紀的女性,開啟了一扇離家的窄門,那麼《海妲.蓋柏樂》展現的便是門後的窒息世界。本篇文章由劇場演員 Amily 執筆,同為熟稔文本的演員,亦是深刻體察制度縫隙的當代女性,此文所看見的不僅僅是崩壞前夕的最後發聲,更是女人被迫置於冷酷的制度之下,步步陷入無以言說的困境。
Thumbnail
若說易卜生的《玩偶之家》為 19 世紀的女性,開啟了一扇離家的窄門,那麼《海妲.蓋柏樂》展現的便是門後的窒息世界。本篇文章由劇場演員 Amily 執筆,同為熟稔文本的演員,亦是深刻體察制度縫隙的當代女性,此文所看見的不僅僅是崩壞前夕的最後發聲,更是女人被迫置於冷酷的制度之下,步步陷入無以言說的困境。
Thumbnail
# 簡介 身為一位專注於 Vue.js 的前端開發者,這是我第一次嘗試構建 Flutter 網頁應用。讓我們開始吧! ## 第一次嘗試 ### 第一步:創建一個 Flutter 應用 首先,通過運行以下命令來創建一個新的 Flutter 項目: ```sh flutter
Thumbnail
# 簡介 身為一位專注於 Vue.js 的前端開發者,這是我第一次嘗試構建 Flutter 網頁應用。讓我們開始吧! ## 第一次嘗試 ### 第一步:創建一個 Flutter 應用 首先,通過運行以下命令來創建一個新的 Flutter 項目: ```sh flutter
Thumbnail
長期以來,西方美學以《維特魯威人》式的幾何比例定義「完美身體」,這種視覺標準無形中成為殖民擴張與種族分類的暴力工具。本文透過分析奈及利亞編舞家庫德斯.奧尼奎庫的舞作《轉轉生》,探討當代非洲舞蹈如何跳脫「標本式」的文化觀看。
Thumbnail
長期以來,西方美學以《維特魯威人》式的幾何比例定義「完美身體」,這種視覺標準無形中成為殖民擴張與種族分類的暴力工具。本文透過分析奈及利亞編舞家庫德斯.奧尼奎庫的舞作《轉轉生》,探討當代非洲舞蹈如何跳脫「標本式」的文化觀看。
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
ListViewItem 交換順序以及拖曳效果。
Thumbnail
ListViewItem 交換順序以及拖曳效果。
追蹤感興趣的內容從 Google News 追蹤更多 vocus 的最新精選內容追蹤 Google News