Flutter中Widget,State和BuildContext的概念是每個Flutter開發(fā)人員需要完全理解的最重要概念之一。這里先講解一下Widget以及Widget。三者之間的關系會在最后一篇總結一下。
Widget詳解
Widget類在Flutter中是非常重要的,繼承自Widget類的有PreferredSizeWidget、ProxyWidget、RenderObjectWidget、StatefulWidget、StatelessWidget。我們日常使用的絕大部分widget都是繼承自Widget類,查看Widget類源碼,內部實現非常簡單,構造函數如下:
const Widget({ this.key });
final Key key;
在flutter中構建APP是由widget樹構建起來的,所以這個key的作用是用來控制在widget樹中替換widget的時候使用的。其中Key類是Widget、Element以及SemanticsNode的唯一標識符,繼承自Key的還有LocalKey以及GlobalKey。詳細可以去framework.dart文件查看相關源碼及說明。
Widget分類
在Flutter中,我們平時自定義的widget,一般都是繼承自StatefulWidget或StatelessWidget(并不是只有這兩種),這兩種widget也是目前最常用的兩種。如果一個控件自身狀態(tài)不會去改變,創(chuàng)建了就直接顯示,不會有色值、大小或者其他屬性的變化,這種widget一般都是繼承自StatelessWidget,常見的有Container、ScrollView等。如果一個控件需要動態(tài)的去改變或者相應一些狀態(tài),例如點擊態(tài)、色值、內容區(qū)域等,那么一般都是繼承自StatefulWidget,常見的有CheckBox、AppBar、TabBar等。兩者的差別在于是否有狀態(tài)。

-
StatelessWidget
一個StatelessWidget可以用多個不同的BuildContext構建,它是由build函數構建的。BuildContext抽象類,它表示一個控件在整個控件樹中的位置句柄,每個控件都有自己的BuildContext實例。某些靜態(tài)函數(例如showDialog、Theme.of等)也有BuildContext實例,以便它們可以代表調用控件或專門針對給定上下文獲取數據。BuildContext對象被傳遞給WidgetBuilder函數,它為創(chuàng)建控件的函數簽名,例如StatelessWidget.build或State.build。無狀態(tài)窗口小部件只能在加載/構建窗口小部件時繪制一次,這意味著無法基于任何事件或用戶操作重繪窗口小部件。
圖片2.png
對于StatelessWidget,build方法會在如下三種情況下調用:
- widget第一次被插入到樹中;
- widget的父節(jié)點更改了配置(configuration);
- widget依賴的InheritedWidget(InheritedWidget是一個特殊的Widget,您可以將其作為另一個子樹的父級放在Widgets樹中。該子樹的所有小部件都必須能夠與該InheritedWidget公開的數據進行交互,后面會詳細總結)改變了。
import 'package:flutter/material.dart';
import 'package:flutter_apptest/SplashPage.dart';
void main() => runApp(new MyApp());
class MyApp extends StatelessWidget {
MyApp ({
Key key,
this.parameter,
}): super(key:key);
final parameter;
@override
Widget build(BuildContext context) {
return new MaterialApp(
theme: new ThemeData(
primarySwatch: Colors.blue,
),
home: SplashPage(),//啟動MainPage
);
}
}
-
StatefulWidget
其他一些小部件將處理一些在Widget生命周期內會發(fā)生變化的內部數據。因此,該數據變得動態(tài)。
此Widget保存的數據集可能會在此Widget的生命周期內發(fā)生變化,稱為State。
這些窗口小部件稱為有狀態(tài)窗口小部件(Stateful Widget)。
這樣的Widget的示例可以是用戶可以選擇的復選框列表或者根據條件禁用的Button。
StatefulWidget創(chuàng)建需要一個State狀態(tài),通過createState函數返回。而一個StatefulWidget會為每個BuildContext創(chuàng)建一個State對象。
圖片3.png
在了解這些狀態(tài)的時候,更多的是了解State狀態(tài)
import 'package:flutter/material.dart';
class MinePage extends StatefulWidget{
//此部分在Widget的生命周期內不會發(fā)生變化,但可能接受可由其相應的State實例使用的參數。請注意,在Widget的第一部分定義的任何變量通常在其生命周期內不會更改。
MyStatefulWidget({
Key key,
this.color,
}): super(key: key);
final Color color;
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return new Page();
}
}
class Page extends State<MinePage>{
@override
void initState() {
// TODO: implement initState
super.initState();
}
@override
Widget build(BuildContext context) {
// TODO: implement build
return layout(context);
}
Widget layout(BuildContext context) {
// 改變狀態(tài)加以重新繪制
setState(() {
});
return new Scaffold(
appBar: buildAppBar(context),
body: new Text('我的頁面'),
);
}
buildAppBar(BuildContext context) {
return new AppBar(title: const Text('我的'),);
}
@override
void dispose() {
// TODO: implement dispose
super.dispose();
}
@override
void didChangeDependencies() {
// TODO: implement didChangeDependencies
super.didChangeDependencies();
}
}
我們在創(chuàng)建State的時候可以看到和StatefulWidget相似的build方法,也就是說我們也可以獲得一個BuildContext,在使用StatefulWidget.createState創(chuàng)建它們之前以及在調用initState之前,框架將State對象與BuildContext關聯起來,該關聯是永久的:State對象永遠不會改變它的BuildContext(但是BuildContext本身可以在控件樹中移動)。后面講解一下這個BuildContext對象在整個程序中什么角色
State
State的作用有兩點:
- 在widget構建的時候可以被同步讀??;
- 在widget的生命周期中可能會被改變。
State的生命周期有四種狀態(tài):
- created:當State對象被創(chuàng)建時候,State.initState方法會被調用;
- initialized:當State對象被創(chuàng)建,但還沒有準備構建時,State.didChangeDependencies在這個時候會被調用;
- ready:State對象已經準備好了構建,State.dispose沒有被調用的時候;
- defunct:State.dispose被調用后,State對象不能夠被構建。

完整生命周期如下:
- 創(chuàng)建一個State對象時,會調用StatefulWidget.createState;
- 和一個BuildContext相關聯,可以認為被加載了(mounted);
- 調用initState;
- 調用didChangeDependencies;
- 經過上述步驟,State對象被完全的初始化了,調用build;
- 如果有需要,會調用didUpdateWidget;
- 如果處在開發(fā)模式,熱加載會調用reassemble;
- 如果它的子樹(subtree)包含需要被移除的State對象,會調用deactivate;
- 調用dispose,State對象以后都不會被構建;
- 當調用了dispose,State對象處于未加載(unmounted),已經被dispose的State 對象沒有辦法被重新加載(remount)。
當控件的配置被更改時會調用State.didUpdateWidget方法,此時框架會重新繪制控件。你也可以使用State.setState方法在狀態(tài)發(fā)生變化時通知框架,告訴框架該對象的內部狀態(tài)已經改變,框架接到通知后也會重新繪制控件。
State常用方法
initState()
initState()方法是創(chuàng)建State對象后要調用的第一個方法(在構造函數之后)。
需要執(zhí)行其他初始化時,將覆蓋重寫此方法。典型的初始化與動畫,控制器有關…
如果重寫此方法,則需要在第一個位置調用super.initState()方法。
在這個方法中,上下文context可用,但你還不能真正使用它,因為框架還沒有完全將狀態(tài)與它相關聯。
initState()方法完成后,State對象現在已初始化,上下文可用。在此State對象的生命周期內不再調用此方法。didChangeDependencies()
didChangeDependencies()方法是要調用的第二個方法。
在此階段,由于上下文可用,您可以使用它。
如果您的Widget鏈接到InheritedWidget和/或您需要初始化一些偵聽器(基于BuildContext),則通常會覆蓋此方法。請注意,如果您的窗口小部件鏈接到InheritedWidget,則每次重建此窗口小部件時都會調用此方法。build()
build(BuildContext context)方法在didChangeDependencies()(和didUpdateWidget)之后調用。
這是您構建窗口小部件(可能還有任何子樹)的位置。
每次State對象更改時(或者當InheritedWidget需要通知“已注冊”的小部件時)都會調用此方法!
為了強制重建,您可以調用setState((){...})方法。dispose()
放棄窗口小部件時調用dispose()方法。
如果你需要執(zhí)行一些清理(例如監(jiān)聽器,控制器…),然后立即調用super.dispose(),則覆蓋此方法。
State.setState
State中比較重要的一個方法是setState,當修改狀態(tài)時,widget會被更新。比方說點擊CheckBox,會出現選中和非選中狀態(tài)之間的切換,就是通過修改狀態(tài)來達到的。查看setState源碼,在一些異常的情況下將會拋出異常:
- 傳入的為null;
- 處在defunct階段;
- created階段還沒有被加載(mounted);
- 參數返回一個Future對象。
檢查完一系列異常后,最后調用代碼如下:
_element.markNeedsBuild();
markNeedsBuild內部,則是通過標記element為diry,在下一幀的時候重建(rebuild)??梢钥闯鰏etState并不是立即生效,它只是將widget進行了標記,真正的rebuild操作,則是等到下一幀的時候才會去進行。
StatefulWidget的兩個主要類別:
- 在initState中創(chuàng)建資源,在dispose中銷毀,但是不依賴于InheritedWidget或者調用setState方法,這類widget基本上用在一個應用或者頁面的root;
- 使用setState或者依賴于InheritedWidget,這種在營業(yè)生命周期中會被重建(rebuild)很多次。
總結一句話就是在每一幀的繪制過程,StatefulWidget會根據State或者配置變了來rebuild下一幀,也就是上面圖所說的弄臟了和下一幀又干凈了。
選擇無狀態(tài)還是有狀態(tài)小部件?
在我的小部件的生命周期中,我是否需要考慮一個將要更改的變量,何時更改,將強制重建小部件?
如果問題的答案是肯定的,那么您需要一個有狀態(tài)的小部件,否則,您需要一個無狀態(tài)小部件。
比如:
- 用于顯示復選框列表的小組件。要顯示復選框,您需要考慮一系列項目。每個項目都是一個具有標題和狀態(tài)的對象。如果單擊復選框,則切換相應的item.status;在這種情況下,您需要使用有狀態(tài)窗口小部件來記住項目的狀態(tài),以便能夠重繪復選框。
- 帶有表格的屏幕。該屏幕允許用戶填寫表單的窗口小部件并將表單發(fā)送到服務器。在這種情況下,在這種情況下,除非您在提交表單之前需要驗證表單或執(zhí)行任何其他操作,否則無狀態(tài)窗口小部件可能就足夠了。

