Flutter開(kāi)發(fā)setState能否在build中直接調(diào)用詳解
兩種情況
setState() 能在 build() 中直接調(diào)用嗎?答案是能也不能。
來(lái)看一段簡(jiǎn)單的代碼:
import 'package:flutter/material.dart'; class TestPage extends StatefulWidget { const TestPage({super.key}); @override State<TestPage> createState() => _State(); } class _State extends State<TestPage> { int _count = 0; @override Widget build(BuildContext context) { setState(() { _count++; }); return Scaffold( appBar: AppBar( title: const Text('測(cè)試頁(yè)面'), ), body: Center( child: Text( '$_count', style: const TextStyle(fontSize: 24), ), ), ); } }
跑起來(lái)后代碼不會(huì)報(bào)錯(cuò),Text('$_count') 顯示結(jié)果是 1,看來(lái) build() 調(diào)用 setState() 沒(méi)啥問(wèn)題呀。小改一下,來(lái)看看這個(gè):
class _State extends State<TestPage> { int _count = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text('測(cè)試頁(yè)面'), ), body: Center( child: Builder( builder: (context) { setState(() { _count++; }); return Text( '$_count', style: const TextStyle(fontSize: 24), ); } ), ), ); } }
改動(dòng)主要是在 Text 上面加了一個(gè) Builder,然后把 setState() 放在了 Builder 的 builder 中去調(diào)用。運(yùn)行起來(lái),結(jié)果出現(xiàn)報(bào)錯(cuò)了:The following assertion was thrown building Builder(dirty): setState() or markNeedsBuild() called during build.
提示在 Builder 的 build() 過(guò)程中出現(xiàn)了斷言錯(cuò)誤:build() 中不能調(diào)用 setState() 或 markNeedsBuild()。
這是什么情況呢,為什么第一種情況下可以在 build() 中調(diào)用 setState() 而第二種情況不行?下面來(lái)簡(jiǎn)單地分析下其中包含的原理。
原理分析
先說(shuō)一下結(jié)論,在 build() 中直接調(diào)用 setState() 要滿足一個(gè)前提條件:
如果當(dāng)前有組件 A 處于 build() 中,那么 setState() 引起 rebuild 的組件必須是 A 或者 A 的子孫組件,不能是 A 的祖先組件。
這是因?yàn)榻M件 build 的順序是從父到子,如果在子組件 build 的過(guò)程中執(zhí)行 setState() 之類會(huì)引起父組件的重新 build 那就死循環(huán)肯定是不行的。
接下來(lái)看下 Flutter 源碼中是如何判斷和控制的。setState() 的內(nèi)部會(huì)調(diào)用 _element!.markNeedsBuild()
,markNeedsBuild()
中有如下代碼:
void markNeedsBuild() { // ... // 前半部分,斷言重新 build 是否滿足上面說(shuō)的前提。 assert(() { if (owner!._debugBuilding) { assert(owner!._debugCurrentBuildTarget != null); assert(owner!._debugStateLocked); // _debugIsInScope() 用來(lái)判斷是否滿足前提條件。 if (_debugIsInScope(owner!._debugCurrentBuildTarget!)) { return true; } if (!_debugAllowIgnoredCallsToMarkNeedsBuild) { final List<DiagnosticsNode> information = <DiagnosticsNode>[ ErrorSummary('setState() or markNeedsBuild() called during build.'), // ... ]; // ... } // ... }()); // ... }
markNeedsBuild() 代碼的前半部分有斷言來(lái)處理是否滿足上面說(shuō)到的前提條件,_debugCurrentBuildTarget
就是當(dāng)前正處于 build 狀態(tài)的 element。_debugCurrentBuildTarget()
的內(nèi)容如下:
bool _debugIsInScope(Element target) { Element? current = this; while (current != null) { if (target == current) { return true; } current = current._parent; } return false; }
_debugIsInScope() 中的 this
就是調(diào)用 setState() 會(huì)引起 rebuild 的組件,target
就是當(dāng)前正處于 build 的組件。其中的 while
循環(huán)會(huì)逐步比對(duì) current 及其父組件是否當(dāng)前 build 的對(duì)象,找到了才會(huì)返回 true,否則就是 false。如果是 false,則后面的斷言就會(huì)出現(xiàn)錯(cuò)誤:setState() or markNeedsBuild() called during build.
如果當(dāng)前有組件正在 build 那么決不能引起父組件的 rebuild,我們來(lái)看下前面舉例報(bào)錯(cuò)的第二種情況。Builder 是 TestPage 的子組件,Builder 的 builder 方法里調(diào)用的 setState 是 TestPage 上的,也就是在子組件的 build 過(guò)程中使父組件 rebuild 了,那么就會(huì)引起斷言失敗;而第一種情況下是在 TestPage 的 build 過(guò)程中調(diào)用 setState 使自己重新 rebuild,可以滿足結(jié)論的前提,所以是可以調(diào)用的。
這里我們可以接著想下在第一種情況下,組件自己的 build 過(guò)程中調(diào)用了 setState 引起了自己重新 rebuild 的時(shí)候不是也會(huì)死循環(huán)了嗎?我們接著看下 markNeedsBuild()
的后半部分代碼,如果斷言成功后后面的邏輯:
void markNeedsBuild() { // ... // 前半部分是斷言。 if (dirty) { return; } _dirty = true; owner!.scheduleBuildFor(this); }
這里可以看到組件在 build 過(guò)程中 markNeedsBuild()
會(huì)使組件變?yōu)?dirty 狀態(tài),這個(gè)時(shí)候在 build 中直接調(diào)用 setState 后發(fā)現(xiàn)已經(jīng)是 dirty 狀態(tài)后會(huì)直接返回,而不會(huì)調(diào)度重新 build,所以就沒(méi)有問(wèn)題了。
總結(jié)
通過(guò)以上的分析我們知道了 Flutter 是如何判斷如果在 build 過(guò)程中直接調(diào)用 setState 是否合法的。當(dāng)然我們?cè)趯?xiě)代碼的時(shí)候是不會(huì)在 build() 中直接調(diào)用 setState 的,了解以上過(guò)程更有助于我們排查問(wèn)題和學(xué)習(xí) Flutter 的運(yùn)行原理。
以上就是Flutter開(kāi)發(fā)setState能否在build中直接調(diào)用詳解的詳細(xì)內(nèi)容,更多關(guān)于Flutter setState調(diào)用build的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android 使用Vitamio打造自己的萬(wàn)能播放器(9)—— 在線播放 (在線電視)
本文主要介紹Android 使用Vitamio開(kāi)發(fā)播放器,實(shí)現(xiàn)在線電視播放,這里提供效果圖和實(shí)例代碼以便大家參考,2016-07-07Android通過(guò)json向MySQL中讀寫(xiě)數(shù)據(jù)的方法詳解【讀取篇】
這篇文章主要介紹了Android通過(guò)json向MySQL中讀寫(xiě)數(shù)據(jù)的方法,涉及Android解析json以及與php交互讀取mysql的方法,需要的朋友可以參考下2016-06-06android開(kāi)機(jī)自啟動(dòng)app示例分享
這篇文章主要介紹了android開(kāi)機(jī)自動(dòng)啟動(dòng)APP的方法,大家參考使用吧2014-01-01Android程序靜默安裝安裝后重新啟動(dòng)APP的方法
這篇文章主要介紹了Android 靜默安裝,安裝后重新啟動(dòng)APP的方法,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-01-01Android打空包后提示沒(méi)有"android:exported"的屬性設(shè)置問(wèn)題解決
這篇文章主要介紹了Android打空包后提示沒(méi)有"android:exported"的屬性設(shè)置問(wèn)題的解決方法,文中通過(guò)圖文將解決的辦法介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2023-02-02Android基于Sqlite實(shí)現(xiàn)注冊(cè)和登錄功能
這篇文章主要為大家詳細(xì)介紹了Android基于Sqlite實(shí)現(xiàn)注冊(cè)和登錄功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-04-04如何使用Flutter開(kāi)發(fā)一款電影APP詳解
這篇文章主要給大家介紹了關(guān)于如何使用Flutter開(kāi)發(fā)一款電影APP的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Flutter具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-07-07