拋磚引玉
自從開始使用Flutter,接觸最多的東西肯定少不了StatefulWidget
和StatelessWidget
。我本人在學習和了解它們的過程中也翻閱了大量的文件和資料,但發現他們都在講二者的區別和使用場景以及案例——但是為什麼要這麼用呢?這是一個值得思考的問題。
StatefulWidget和StatelessWidget簡介
免不了俗,開篇也是先講一下StatefulWidget
和StatelessWidget
的用法和區別吧。
Flutter中,一切皆Widget。Widget是檢視的載體,而Widget包含兩種,一種是不需要更改狀態的Widget,StatelessWidget
它沒要需要管理的內部狀態,是無狀態的。另外一種是可變狀態的,StatefulWidget
它有需要管理的內部狀態,使用setState
來管理狀態改變。
Widget是有狀態的還是無狀態的,取決於他們依賴於狀態的變化:
- 有狀態:互動或者資料改變導致Widget改變,例如改變文案
- 無狀態:不會被改變的Widget,例如純展示頁面,資料也不會改變
特別提示
我特意用一個標題來吸引大家注意,是因為我在好幾篇部落格看到了類似下面的話:
Flutter 裡面包含兩種widget,一種是不可變的Widget——StatelessWidget,另外一種是可變的Widget——
StatefulWidget
這是大錯特錯的!!!,因為Widget只是檢視的“配置資訊”,是資料的對映, Widget
是不可變的,不可變的!!。變的只是Widge裡面的狀態,也就是State。
貼一段Widget
原始碼的截圖
“A widget is an immutable description of part of a user interface”
。Widget只是使用者介面一部分不可變的描述——至於為什麼不可變以及都不可變了還怎麼重新整理UI,這兩個問題接下來我會用一片部落格詳細介紹一下Flutter的渲染機制。
StatelessWidget
StatelessWidget是一個沒有狀態的widget——沒有要管理的內部狀態。它通過構建一系列其他小部件來更加具體地描述使用者介面,從而描述使用者介面的一部分。當我們的頁面不依賴Widget物件本身中的配置資訊以及BuildContext時,就可以用到無狀態元件。例如當我們只需要顯示一段文字時。實際上Icon、Divider、Dialog、Text等都是StatelessWidget的子類。
StatelessWidget
的基本使用如下:
class Less extends StatelessWidget {
final String text;
const Less({Key key, this.text}) : super(key: key);
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Text(text);
}
}
複製程式碼
Less
包含了一個從外部接受一個不可變的資料來源text並將它顯示。
無狀態的元件的宣告週期只有一個:build
,它只會在三種情況下被呼叫:
- 將widget插入樹中的時候,也就是第一次構建
- 當widget的父級更改了其配置時,例如,Less的父類改變了text的值
- 當它依賴的InheritedWidget發生變化時
StatefulWidget
StatefulWidget是可變狀態的widget。使用setState方法管理StatefulWidget的狀態的改變。呼叫setState通知Flutter框架某個狀態發生了變化,Flutter會重新執行build方法,應用程式變可以顯示最新的狀態。
狀態是在構建widget的時候,widget可以同步讀取的資訊,而這些狀態會發生變化。要確保在狀態改變的時候即使通知widget進行動態更改,就需要用到StatefulWidget
。例如一個計數器,我們點選按鈕就要讓數字加一。在Flutter中,Checkbox、FadeImage等都是有狀態元件。
StatefulWidget
的基本使用如下:
class Full extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _Full();
}
}
class _Full extends State<Full> {
int count = 0;
@override
Widget build(BuildContext context) {
// TODO: implement build
return new GestureDetector(
onTap: onClick,
child: new Text("$count"),
);
}
void onClick() {
setState(() {
count += 1;
});
}
}
複製程式碼
Full
包含了一個內部持有的int狀態,每次點選自增一,平使用setState重新整理頁面顯示最新的值。
StatefulWidget
的生命週期比較複雜,有興趣的可以去看我的另一篇部落格:Flutter檢視Widget生命週期
StatefulWidget和StatelessWidget的實用場景
在涉及到Widget的工作時,遇到的頭等大事就是確定widget應該使用StatefulWidget還是StatelessWidget?
簡單的說,如果不需要自己維持狀態就使用StatelessWidget
,否則使用StatefulWidget
。
進一步分析,根據上文的介紹,我們不難發現:
- 如果使用者互動或資料改變導致widget改變,那麼它就是有狀態的。
- 如果一個widget是最終的或不可變的,那麼它就是無狀態。
而狀態的管理可能有三種方式——自己管理,父widget管理以及兩者混合搭配。我們可以參考下面的規則選擇Widget:
- 如果widget的狀態取決於動作,那麼最好是由widget自身來管理狀態,也就是使用
StatefulWidget
,例如動畫; - 如果狀態是使用者資料,則最好用父widget管理,也即是使用
StatelessWidget
,例如一個列表單個Item的選中狀態; - 如果還是搖擺不定,別問,問就是
StatelessWidget
。
更進一步的思考
前面我們已經知道了,Widget是不可變的,如果要改變就要重新建立。而StatefulWidget使用State來通過控制自身狀態來為自己標記狀態,這樣就可以在下一次系統重繪檢查時重新建立。
為什麼要選用StatelessWidget
通過上面的介紹,大家不難發現StatefulWidget幾乎是這樣一個存在——我在任何需求下使用它都能實現想要的效果,那麼我們為什麼不一股腦全部使用它呢?既然它也能實現StatelessWidget的效果,那我們還要StatelessWidget做什麼?StatefulWidget就是一個全能的存在啊!! 為了解釋這個疑問,我們就要去了解一下StatefulWidget伴隨著全能而來的代價! 首先我們粗略的追溯一下setState的重新整理原始碼:
@protected
void setState(VoidCallback fn) {
...
_element.markNeedsBuild();
}
void markNeedsBuild() {
...
if (dirty)
return;
_dirty = true;
owner.scheduleBuildFor(this);
}
void scheduleBuildFor(Element element) {
...
if (!_scheduledFlushDirtyElements && onBuildScheduled != null) {
_scheduledFlushDirtyElements = true;
onBuildScheduled();
}
_dirtyElements.add(element);
element._inDirtyList = true;
...
}
複製程式碼
去掉所有的assert校驗,只保留關鍵程式碼,我們發現setState會呼叫element
的markNeedsBuild
方法,用來標記當前element
為dirty狀態,也就是需要build。並執行BuildOwner
的scheduleBuildFor
方法,BuildOwner
是負責管理element
的。直接追溯到onBuildScheduled
,該發放的實現為widget/binding.dart
中的_handleBuildScheduled
方法,其中呼叫了scheduler/binding.dart
中的ensureVisualUpdate
,最後呼叫了scheduleFrame
方法:
void scheduleFrame() {
if (_hasScheduledFrame || !_framesEnabled)
return;
assert(() {
if (debugPrintScheduleFrameStacks)
debugPrintStack(label: 'scheduleFrame() called. Current phase is $schedulerPhase.');
return true;
}());
window.scheduleFrame();
_hasScheduledFrame = true;
}
複製程式碼
關鍵的程式碼出現了:window.scheduleFrame()
這是一個Native方法:
實際上setState只是用來標記state物件需要根據已經變更的狀態重新build來建立新的widget。呼叫setState將會出發每個子Widget的構造方法以及build方法。這意味著如果根佈局是一個StatefulWidget
,那麼setState之後,整個頁面所有的widget都會重建。
通過程式碼來驗證一下:
class FulBackPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _FulBackPage();
}
}
class _FulBackPage extends State<FulBackPage> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Column(
children: <Widget>[
Full(name: "A"),
Full(name: "B"),
Full(name: "C"),
Full(name: "D"),
Less(name: "E"),
GestureDetector(
onTap: () {
setState(() {});
},
child: Text("點選"),
)
],
);
}
}
class Full extends StatefulWidget {
final String name;
Full({Key key, this.name}) : super(key: key) {
print("有狀態元件$name:建立了");
}
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _Full();
}
}
class _Full extends State<Full> {
@override
Widget build(BuildContext context) {
print("有狀態元件${widget.name}:build了");
return new GestureDetector(
onTap: () {
setState(() {});
},
child: new Text(widget.name),
);
}
}
class Less extends StatelessWidget {
final String name;
Less({Key key, this.name}) : super(key: key){
print("無狀態元件$name:建立了");
}
@override
Widget build(BuildContext context) {
// TODO: implement build
print("無狀態元件$name:build了");
return new Text(name);
}
}
複製程式碼
每次點選FulBackPage
的按鈕重新整理頁面,日誌輸出如下:
我編不下去了啊!!!翻了翻兩個widget的build原始碼,除了一個多了個state之外,我也沒發現什麼端倪。 總之:
- 優先使用StatelessWidget
- 含有大量子Widget(如根佈局、次根佈局)最好使用StatelessWidget
- StatefulWidget最好用在子節點,同時儘量減少它的子節點。
總結
開篇丟擲的問題我還是沒有徹底想明白: