開發(fā)者在進(jìn)行Flutter開發(fā)時少孝,大部分工作基本上少不了與StatelessWidget和StatefulWidget打交道邑退。大家是否真的了解StatelessWidget和StatefulWidget?
討論
我閱讀了很多網(wǎng)上的文章,大部分會講解兩者的使用上的區(qū)別劳澄,一部分文章有解釋這兩者的區(qū)別瓜饥。但是他們的解釋有的是字面解釋,有的是淺嘗輒止浴骂,有的甚至是有一定的誤導(dǎo)乓土。
列出網(wǎng)上一些文章中的解釋:
- 如果我們的Widget是StatelessWidget,那么當(dāng)他的內(nèi)容被創(chuàng)建出來之后溯警,就不能再改變了趣苏。相反StatefulWidget就可以。
- 無狀態(tài)Widget梯轻,就是說一旦這個Widget創(chuàng)建完成食磕,狀態(tài)就不允許再變動。有狀態(tài)Widget喳挑,就是說當(dāng)前Widget創(chuàng)建完成之后彬伦,還可以對當(dāng)前Widget做更改,可以通過setState函數(shù)來刷新當(dāng)前Widget來達(dá)到有狀態(tài)伊诵。
- StatelessWidget是一個不需要狀態(tài)更改的widget单绑,它沒有要管理的內(nèi)部狀態(tài)。StatefulWidget是可變狀態(tài)的widget曹宴。
如果你對上述一些觀點很認(rèn)同的話搂橙,我覺得閱讀本篇文章應(yīng)該可以給你提供一個不一樣的理解視角。
Widget
我們要比較StatelessWidget和StatefulWidget的區(qū)別笛坦,我們得先知道什么是Widget区转。
官方對Widget的解釋是:
A widget is an immutable description of a part of a user interface.
即Widget是部分界面的不可變的描述信息。
重要的事情說三遍:
Widget是不可變的;
Widget是不可變的;
Widget是不可變的版扩。
我們從代碼上看看Widget如何實現(xiàn)的不可變废离。Widget的代碼如下:
@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key? key;
// 省略...
}
我們可以看到Widget左上角有一個@immutable
注解,這個注解的意思是所有的屬性必須是final修飾礁芦,也就是Widget一旦初始化以后蜻韭,其屬性將不可變。
接下來我們再看看StatelessWidget和StatefulWidget的官方解釋和相關(guān)代碼:
StatelessWidget --- A widget that does not require mutable state.
abstract class StatelessWidget extends Widget {
const StatelessWidget({ Key? key }) : super(key: key);
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}
StatefulWidget --- A widget that has mutable state.
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key? key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
@factory
State createState();
}
Widget總結(jié)
- StatelessWidget和StatefulWidget沒有本質(zhì)區(qū)別宴偿,他們的所有屬性都是不可變的湘捎。它們都沒法更新,除非用一個新的Widget去替換它們窄刘。
- StatefulWidget擁有一個可變的State窥妇。
這樣我們就得到了一個結(jié)論:StatelessWidget和StatefulWidget的區(qū)別就在這個可變的State了。
新的問題又來了娩践,這個State扮演了什么作用呢活翩?
State
我們進(jìn)行界面的修改烹骨,一般會調(diào)用state.setState()
方法。那這個方法是如何實現(xiàn)界面元素修改的呢材泄?
void setState(VoidCallback fn) {
final dynamic result = fn() as dynamic;
_element!.markNeedsBuild();
}
setState方法很簡單:
- 執(zhí)行傳入的函數(shù);
-
_element
調(diào)用了markNeedsBuild
方法沮焕。
void markNeedsBuild() {
if (dirty)
return;
_dirty = true;
owner!.scheduleBuildFor(this);
}
-
_element
把自己的_dirty
屬性設(shè)置為true; -
BuildOwner調(diào)用
scheduleBuildFor
方法。
void scheduleBuildFor(Element element) {
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
// 1
onBuildScheduled!();
}
_dirtyElements.add(element);
element._inDirtyList = true;
}
-
BuildOwner調(diào)用
onBuildScheduled
方法拉宗;
內(nèi)容回顧:
onBuildScheduled
方法是在WidgetsBinding的initInstances中初始化的,一系列調(diào)用后最后調(diào)用的就是scheduleFrame
請求Native Platform要刷新界面峦树。
- 將element加入到dirtyElements中。
在合適的時候Flutter Engine會回調(diào)SchedulerBinding的handleDrawFrame
方法旦事,最后會調(diào)用BuildOwner的buildScope
方法魁巩。
void buildScope(Element context, [ VoidCallback? callback ]) {
int dirtyCount = _dirtyElements.length;
int index = 0;
while (index < dirtyCount) {
_dirtyElements[index].rebuild();
}
}
遍歷dirtyElements元素,每個element調(diào)用rebuild
姐浮。
rebuild的作用是什么谷遂?沒錯,就是我們開頭提到的對界面元素進(jìn)行更新的操作卖鲤。
刷新渲染的具體邏輯肾扰,我將會在后續(xù)文章中詳細(xì)介紹,這里沒法詳細(xì)展開蛋逾。
結(jié)論
StatelessWidget和StatefulWidget的本質(zhì)區(qū)別就是能否自我重新構(gòu)建(self rebuild)集晚。
一些思考
- 既然StatefulWidget的主要作用只是為了賦予了其自我重新構(gòu)建(self rebuild)的能力,那為什么需要State呢换怖?
Widget依賴于構(gòu)造函數(shù)和Build方法中的
BuildContext
中的外部信息甩恼,如果是外部觸發(fā)的Build(例如:祖先Widget build),所有信息都是完整的沉颂。如果self rebuild則無法獲取更新后的外部信息,所以需要內(nèi)部維護(hù)一份不依賴于外部的信息悦污,State就是這個作用铸屉。
- 既然StatefulWidget的功能更完善,為什么又提供一個StatelessWidget呢切端?
這個問題其實等同于為什么官方要限制我們使用self rebuild彻坛?每次Build都需要新建和銷毀大量的Widget,Element Tree的diff踏枣,甚至繁重的渲染和重繪昌屉。官方推薦使用StatelessWidget,其實就是為了性能的考慮而對開發(fā)者進(jìn)行的一些約束茵瀑,限制開發(fā)者無節(jié)制的使用self rebuild造成的性能降低间驮。
- 可不可以在開發(fā)中全部都使用StatefulWidget?
當(dāng)然可以马昨,但是不推薦竞帽,理由見上個問題扛施。
- 可不可以在開發(fā)中全部都使用StatelessWidget?
如果是顯示簡單的不變的內(nèi)容可以這樣使用屹篓,但是這種場景太少了疙渣。至少在App應(yīng)用中不太可能。
- 開發(fā)中如何選擇StatelessWidget還是StatefulWidget堆巧?
首選StatelessWidget妄荔,當(dāng)無法滿足需求的時候用VS Code或者Android Stutio的快捷鍵將其變成StatefulWidget。
實戰(zhàn)分享
我們前面比較了StatelessWidget和StatefulWidget的區(qū)別谍肤,進(jìn)行了一些分析懦冰,到底如何寫出更好更優(yōu)化的代碼,現(xiàn)在我們就用Flutter官方的計數(shù)器Demo來練練手谣沸。
通過前面的分析刷钢,我們知道點擊FloatingActionButton會調(diào)用_MyHomePageState的setState
進(jìn)行rebuild。如下圖所示:
細(xì)心的你可能發(fā)現(xiàn)問題了乳附,我只是想修改Scaffold->Body->Center->Column->第二個Text中的文字内地。而Build的起點是Scaffold,這么長的構(gòu)建鏈條相當(dāng)于修改一個文字赋除,把整個頁面都重新構(gòu)建了一次阱缓。就顯然是一個無法忽視的問題。
注意:真實的Build鏈條不是我上面列的這么短举农,因為Scaffold等都進(jìn)行了封裝荆针,真實的Build得進(jìn)入他們的
build
方法去了解,真實的Build鏈條比我們代碼中看到的Scaffold->Body->Center->Column->第二個Text這個邏輯復(fù)雜多了颁糟。
修改的思路就是我們只需要在第二個Text上封裝一個StatefulWidget航背,讓這個StatefulWidget的setState去觸發(fā)第二個Text的文字修改。
我們抽提一個CounterText:
class CounterText extends StatefulWidget {
final _CounterTextState state = _CounterTextState();
CounterText({
Key key,
}) : super(key: key);
@override
_CounterTextState createState() => state;
}
class _CounterTextState extends State<CounterText> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
return Container(
child: Text(
'$_counter',
style: Theme.of(context).textTheme.headline4,
),
);
}
}
CounterText的使用:
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
CounterText counterText = CounterText();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'You have pushed the button this many times:',
),
counterText,
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: counterText.state._incrementCounter,
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
}
這樣就改造完成了棱貌。
總結(jié):
我們需要對StatelessWidget和StatefulWidget有一個全面的了解玖媚,才能正確的使用他們。歡迎一起探討和學(xué)習(xí)婚脱。