本文介紹了Flutter應用程序中Widget,State,BuildContext和InheritedWidget的重要概念。html
特別注意InheritedWidget,它是最重要且記錄較少的小部件之一。java
本文內容很長,但作技術就是要沉得下心!react
難度:初學者git
Flutter中Widget,State和BuildContext的概念是每一個Flutter開發人員須要徹底理解的最重要概念之一。 可是,文檔很龐大,並不老是清楚地解釋這個概念。github
我會用本身的話語和捷徑來解釋這些概念,本文的真正目的是試圖澄清如下主題:bash
在Flutter中,幾乎全部東西都是Widget。服務器
將Widget視爲可視組件(或與應用程序的可視方面交互的組件)。app
當您須要構建與佈局直接或間接相關的任何內容時,您正在使用窗口小部件。框架
窗口小部件以樹形結構組織。less
包含其餘小部件的小部件稱爲父Widget(或Widget容器)。
包含在父窗口小部件中的窗口小部件稱爲子窗口小部件。
讓咱們用Flutter自動生成的基本應用程序來講明這一點。
這是簡化的代碼,僅限於構建方法:
@override
Widget build(BuildContext){
return new Scaffold(
appBar: new AppBar(
title: new Text(widget.title),
),
body: new Center(
child: new Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
new Text(
'You have pushed the button this many times:',
),
new Text(
'$_counter',
style: Theme.of(context).textTheme.display1,
),
],
),
),
floatingActionButton: new FloatingActionButton(
onPressed: _incrementCounter,
tooltip: 'Increment',
child: new Icon(Icons.add),
),
);
}
複製代碼
若是咱們如今觀察這個基本示例,咱們將看到如下Widgets樹結構(限制代碼中存在的Widgets列表):
另外一個重要的概念是BuildContext。
BuildContext只不過是對構建的全部窗口小部件的樹結構中的窗口小部件的位置的引用。
簡而言之,將BuildContext視爲Widgets樹的一部分,Widget將附加到此樹。
一個BuildContext只屬於一個小部件。
若是窗口小部件「A」具備子窗口小部件,則窗口小部件「A」的BuildContext將成爲直接子窗口BuildContexts的父BuildContext。
閱讀本文,很明顯BuildContexts是連接的,而且正在組成BuildContexts樹(父子關係)。
若是咱們如今嘗試在上圖中說明BuildContext的概念,咱們能夠看到(仍然是一個很是簡化的視圖),其中每種顏色表明一個BuildContext(除了MyApp,它是不一樣的):
BuildContext可見性(簡化語句): 「 Something 」僅在其本身的BuildContext或其父BuildContext的BuildContext中可見。
從這個語句咱們能夠從子BuildContext派生出來,很容易找到一個祖先(= parent)Widget。
一個例子是,考慮Scaffold> Center> Column> Text: context.ancestorWidgetOfExactType(Scaffold)=>經過從Text上下文轉到樹結構來返回第一個Scaffold。
從父BuildContext,也能夠找到一個後代(=子)Widget,但不建議這樣作(咱們稍後會討論)
小部件有兩種類型:
這些可視組件中的一些除了它們本身的配置信息以外不依賴於任何其餘信息,該信息在其直接父級構建時提供。
換句話說,這些小部件一旦建立就沒必要關心任何變化。
這些小部件稱爲無狀態小部件。
這種小部件的典型示例能夠是Text,Row,Column,Container ......其中,在構建時,咱們只是將一些參數傳遞給它們。
參數能夠是裝飾,尺寸甚至其餘小部件中的任何內容。沒關係。惟一重要的是這個配置一旦應用,在下一個構建過程以前不會改變。
無狀態窗口小部件只能在加載/構建窗口小部件時繪製一次,這意味着沒法基於任何事件或用戶操做重繪窗口小部件。
如下是與無狀態小組件相關的代碼的典型結構。
如您所見,咱們能夠將一些額外的參數傳遞給它的構造函數。可是,請記住,這些參數不會在稍後階段發生變化(變異),只能按原樣使用
class MyStatelessWidget extends StatelessWidget {
MyStatelessWidget({
Key key,
this.parameter,
}): super(key:key);
final parameter;
@override
Widget build(BuildContext context){
return new ...
}
}
複製代碼
即便有另外一種方法能夠被重寫(createElement),後者幾乎從不被重寫。 惟一須要被重寫的是build
。
這種無狀態小部件的生命週期很簡單:
其餘一些小部件將處理一些在Widget生命週期內會發生變化的內部數據。所以,該數據變得動態。
此Widget保存的數據集可能會在此Widget的生命週期內發生變化,稱爲State。
這些窗口小部件稱爲有狀態窗口小部件(Stateful Widget)。
這樣的Widget的示例能夠是用戶能夠選擇的複選框列表或者根據條件禁用的Button。
State定義StatefulWidget實例的「行爲」部分。 它包含旨在與Widget交互/干擾的信息:
應用於狀態的任何更改都會強制Widget重建。
對於有狀態窗口小部件,狀態與BuildContext關聯。
此關聯是永久性的 ,State對象永遠不會更改其BuildContext。 即便能夠在樹結構周圍移動Widget BuildContext,State仍將與該BuildContext保持關聯。
當State與BuildContext關聯時,State被視爲已掛載。
重點: 因爲State對象與BuildContext相關聯,這意味着State對象不能(直接)經過另外一個BuildContext訪問!(咱們將在稍後討論這個問題)。
這是與Stateful Widget相關的典型代碼結構。
因爲本文的主要目的是用「變量」數據來解釋State的概念,我將故意跳過與某些Stateful Widget overridable方法相關的任何解釋,這些方法與此沒有特別的關係。
這些可覆蓋的方法是didUpdateWidget,deactivate,reassemble
。這些將在另外一篇文章中討論。
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({
Key key,
this.parameter,
}): super(key: key);
final parameter;
@override
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
@override
void initState(){
super.initState();
// Additional initialization of the State
}
@override
void didChangeDependencies(){
super.didChangeDependencies();
// Additional code
}
@override
void dispose(){
// Additional disposal code
super.dispose();
}
@override
Widget build(BuildContext context){
return new ...
}
}
複製代碼
下圖顯示了與建立有狀態窗口小部件相關的操做/調用序列(簡化版本)。在圖的右側,您將注意到流中的State對象的內部狀態。您還將看到上下文與狀態關聯的時刻,從而變爲可用(已安裝)。
因此讓咱們用一些額外的細節來解釋它:
initState()方法是建立State對象後要調用的第一個方法(在構造函數以後)。
須要執行其餘初始化時,將覆蓋重寫此方法。典型的初始化與動畫,控制器有關...... 若是重寫此方法,則須要在第一個位置調用super.initState()方法。
在這個方法中,上下文context
可用,但你還不能真正使用它,由於框架尚未徹底將狀態與它相關聯。
initState()方法完成後,State對象如今已初始化,上下文可用。
在此State對象的生命週期內再也不調用此方法。
didChangeDependencies()
方法是要調用的第二個方法。
在此階段,因爲上下文可用,您可使用它。
若是您的Widget連接到InheritedWidget和/或您須要初始化一些偵聽器(基於BuildContext),則一般會覆蓋此方法。
請注意,若是您的窗口小部件連接到InheritedWidget,則每次重建此窗口小部件時都會調用此方法。
若是重寫此方法,則應首先調用super.didChangeDependencies()
。
build(BuildContext context)
方法在didChangeDependencies()
(和didUpdateWidget
)以後調用。
這是您構建窗口小部件(可能還有任何子樹)的位置。
每次State對象更改時(或者當InheritedWidget須要通知「已註冊」的小部件時)都會調用此方法! 爲了強制重建,您能夠調用setState((){...})
方法。
放棄窗口小部件時調用dispose()方法。
若是你須要執行一些清理(例如監聽器,控制器......),而後當即調用super.dispose()
,則覆蓋此方法。
這是許多開發人員須要問本身的問題:「我是否須要個人Widget無狀態或有狀態?」 爲了回答這個問題,請問本身:
在個人小部件的生命週期中,我是否須要考慮一個將要更改的變量,什麼時候更改,將強制重建小部件?
若是問題的答案是確定的,那麼您須要一個有狀態的小部件,不然,您須要一個無狀態小部件。 一些例子:
item.status
;在這種狀況下,您須要使用有狀態窗口小部件來記住項目的狀態,以便可以重繪複選框。還記得Stateful小部件的結構嗎?有兩個部分:
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({
Key key,
this.color,
}): super(key: key);
final Color color;
@override
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
複製代碼
第一部分「MyStatefulWidget」一般是Widget的公共部分。當您要將其添加到窗口小部件樹時,能夠實例化此部件。
此部分在Widget的生命週期內不會發生變化,但可能接受可由其相應的State實例使用的參數。
請注意,在Widget的第一部分定義的任何變量一般在其生命週期內不會更改。
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
...
@override
Widget build(BuildContext context){
...
}
}
複製代碼
第二部分「_MyStatefulWidgetState」是在Widget的生命週期中變化的部分,並強制每次應用修改時重建Widget的這個特定實例。
名稱以_
開頭的字符使其成爲.dart文件的私有。 若是須要在.dart文件以外引用此類,請不要使用「_」前綴。
_MyStatefulWidgetState類能夠訪問存儲在MyStatefulWidget中的任何變量,使用widget.{變量的名稱}
。 例如:widget.color
在Flutter中,每一個Widget都是惟一標識的。這個惟一標識由構建/渲染時的框架定義。
此惟一標識對應於可選的Key參數。若是省略,Flutter將爲您生成一個。
在某些狀況下,您可能須要強制使用此密鑰,以即可以經過其密鑰訪問窗口小部件。
爲此,您可使用如下幫助程序之一:GlobalKey ,LocalKey,UniqueKey 或ObjectKey。
該GlobalKey確保關鍵是在整個應用程序惟一的。 強制使用Widget的惟一標識:
GlobalKey myKey = new GlobalKey();
...
@override
Widget build(BuildContext context){
return new MyWidget(
key: myKey
);
}
複製代碼
如前所述,State連接到一個BuildContext,BuildContext連接到Widget的一個實例。
理論上,惟一可以訪問狀態的是Widget State自己。
在這種狀況下,沒有困難。Widget State類訪問其任何變量。
有時,父窗口小部件可能須要訪問其直接子節點的狀態才能執行特定任務。 在這種狀況下,要訪問這些直接子項State,您須要瞭解它們。
給某人打電話的最簡單方法是經過一個名字。在Flutter中,每一個Widget都有一個惟一的標識,由框架在構建/渲染時肯定。如前所示,您可使用key參數強制使用Widget的標識。
...
GlobalKey<MyStatefulWidgetState> myWidgetStateKey = new GlobalKey<MyStatefulWidgetState>();
...
@override
Widget build(BuildContext context){
return new MyStatefulWidget(
key: myWidgetStateKey,
color: Colors.blue,
);
}
複製代碼
一旦肯定,父Widget能夠經過如下方式訪問其子級的狀態:
myWidgetStateKey.currentState
讓咱們考慮一個基本示例,當用戶點擊按鈕時顯示SnackBar。 因爲SnackBar是Scaffold的子Widget,它不能直接被Scaffold身體的任何其餘孩子訪問(還記得上下文的概念及其層次結構/樹結構嗎?)。所以,訪問它的惟一方法是經過ScaffoldState,它公開一個公共方法來顯示SnackBar。
class _MyScreenState extends State<MyScreen> {
/// the unique identity of the Scaffold
final GlobalKey<ScaffoldState> _scaffoldKey = new GlobalKey<ScaffoldState>();
@override
Widget build(BuildContext context){
return new Scaffold(
key: _scaffoldKey,
appBar: new AppBar(
title: new Text('My Screen'),
),
body: new Center(
new RaiseButton(
child: new Text('Hit me'),
onPressed: (){
_scaffoldKey.currentState.showSnackBar(
new SnackBar(
content: new Text('This is the Snackbar...'),
)
);
}
),
),
);
}
}
複製代碼
假設您有一個屬於另外一個Widget的子樹的Widget,以下圖所示。
爲了實現這一目標,須要知足3個條件:
爲了公開它的狀態,Widget須要在建立時記錄它,以下所示:
class MyExposingWidget extends StatefulWidget {
MyExposingWidgetState myState;
@override
MyExposingWidgetState createState(){
myState = new MyExposingWidgetState();
return myState;
}
}
複製代碼
2.「Widget State」須要暴露一些getter / setter 爲了讓「stranger」設置/獲取狀態屬性,Widget State須要經過如下方式受權訪問:
例如:
class MyExposingWidgetState extends State<MyExposingWidget>{
Color _color;
Color get color => _color;
...
}
複製代碼
3.「想要得到State的Widget」(上圖中藍色的widget)須要引用State
class MyChildWidget extends StatelessWidget {
@override
Widget build(BuildContext context){
final MyExposingWidget widget = context.ancestorWidgetOfExactType(MyExposingWidget);
final MyExposingWidgetState state = widget?.myState;
return new Container(
color: state == null ? Colors.blue : state.color,
);
}
}
複製代碼
這個解決方案很容易實現,但子窗口小部件如何知道它什麼時候須要重建? 在這個解決方案,它不知道。它必須等待重建才能刷新其內容,這不是很方便。 下一節將討論Inherited Widget的概念,它能夠解決這個問題。
簡而言之,InheritedWidget容許在窗口小部件樹中有效地傳播(和共享)信息。
InheritedWidget是一個特殊的Widget,您能夠將其做爲另外一個子樹的父級放在Widgets樹中。該子樹的全部小部件都必須可以與該InheritedWidget公開的數據進行交互。
爲了解釋它,讓咱們看下代碼:
class MyInheritedWidget extends InheritedWidget {
MyInheritedWidget({
Key key,
@required Widget child,
this.data,
}): super(key: key, child: child);
final data;
static MyInheritedWidget of(BuildContext context) {
return context.inheritFromWidgetOfExactType(MyInheritedWidget);
}
@override
bool updateShouldNotify(MyInheritedWidget oldWidget) => data != oldWidget.data;
}
複製代碼
此代碼定義了一個名爲「MyInheritedWidget」的Widget,旨在「共享」全部小部件(與子樹的一部分)中的某些數據。
如前所述,爲了可以傳播/共享一些數據,須要將InheritedWidget定位在窗口小部件樹的頂部,這解釋了傳遞給InheritedWidget基礎構造函數的「@required Widget child」。
「static MyInheritedWidget(BuildContext context)」方法容許全部子窗口小部件獲取最接近上下文的MyInheritedWidget的實例(參見後面)
最後,「updateShouldNotify」重寫方法用於告訴InheritedWidget是否必須將通知傳遞給全部子窗口小部件(已註冊/已訂閱),若是對數據應用了修改(請參閱下文)。
所以,咱們須要將它放在樹節點級別,以下所示:
class MyParentWidget... {
...
@override
Widget build(BuildContext context){
return new MyInheritedWidget(
data: counter,
child: new Row(
children: <Widget>[
...
],
),
);
}
}
複製代碼
在構建子child時,後者將得到對InheritedWidget的引用,以下所示:
class MyChildWidget... {
...
@override
Widget build(BuildContext context){
final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
/// 今後刻開始,窗口小部件可使用MyInheritedWidget公開的數據
/// 經過調用:inheritedWidget.data
return new Container(
color: inheritedWidget.data.color,
);
}
}
複製代碼
請考慮如下顯示窗口小部件樹結構的圖表。
爲了說明一種交互方式,咱們假設以下:
InheritedWidget就是用來幹這個的Widget!
代碼示例咱們先寫下代碼,而後解釋以下:
class Item {
String reference;
Item(this.reference);
}
class _MyInherited extends InheritedWidget {
_MyInherited({
Key key,
@required Widget child,
@required this.data,
}) : super(key: key, child: child);
final MyInheritedWidgetState data;
@override
bool updateShouldNotify(_MyInherited oldWidget) {
return true;
}
}
class MyInheritedWidget extends StatefulWidget {
MyInheritedWidget({
Key key,
this.child,
}): super(key: key);
final Widget child;
@override
MyInheritedWidgetState createState() => new MyInheritedWidgetState();
static MyInheritedWidgetState of(BuildContext context){
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
}
class MyInheritedWidgetState extends State<MyInheritedWidget>{
/// List of Items
List<Item> _items = <Item>[];
/// Getter (number of items)
int get itemsCount => _items.length;
/// Helper method to add an Item
void addItem(String reference){
setState((){
_items.add(new Item(reference));
});
}
@override
Widget build(BuildContext context){
return new _MyInherited(
data: this,
child: widget.child,
);
}
}
class MyTree extends StatefulWidget {
@override
_MyTreeState createState() => new _MyTreeState();
}
class _MyTreeState extends State<MyTree> {
@override
Widget build(BuildContext context) {
return new MyInheritedWidget(
child: new Scaffold(
appBar: new AppBar(
title: new Text('Title'),
),
body: new Column(
children: <Widget>[
new WidgetA(),
new Container(
child: new Row(
children: <Widget>[
new Icon(Icons.shopping_cart),
new WidgetB(),
new WidgetC(),
],
),
),
],
),
),
);
}
}
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item');
},
),
);
}
}
class WidgetB extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context);
return new Text('${state.itemsCount}');
}
}
class WidgetC extends StatelessWidget {
@override
Widget build(BuildContext context) {
return new Text('I am Widget C');
}
}
複製代碼
說明
在這個很是基本的例子中,
這一切如何運做? 註冊Widget以供之後通知
當子Widget調用MyInheritedWidget.of(context)時,它會調用MyInheritedWidget的如下方法,並傳遞本身的BuildContext。
static MyInheritedWidgetState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
複製代碼
在內部,除了簡單地返回MyInheritedWidgetState的實例以外,它還將消費者窗口小部件訂閱到更改通知。
在場景後面,對這個靜態方法的簡單調用實際上作了兩件事:
過程
因爲'Widget A'和'Widget B'都已使用InheritedWidget訂閱,所以若是對_MyInherited應用了修改,則當單擊Widget A的RaisedButton時,操做流程以下(簡化版本):
嗯,就是這麼幹的 !
可是,Widget A和Widget B都重建了,而重建Wiget A沒用,由於它沒有任何改變。如何防止這種狀況發生? 在仍然訪問「繼承的」小組件時阻止某些小組件重建
Widget A也被重建的緣由來自它訪問MyInheritedWidgetState的方式。 正如咱們以前看到的,調用context.inheritFromWidgetOfExactType()
方法的其實是自動將Widget訂閱到「使用者」列表。
防止此自動訂閱同時仍容許Widget A訪問MyInheritedWidgetState的解決方案是更改MyInheritedWidget的靜態方法,以下所示:
static MyInheritedWidgetState of([BuildContext context, bool rebuild = true]){
return (rebuild ? context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited
: context.ancestorWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
複製代碼
經過添加布爾類型的額外參數...
所以,要完成解決方案,咱們還須要稍微更新Widget A的代碼,以下所示(咱們添加false額外參數):
class WidgetA extends StatelessWidget {
@override
Widget build(BuildContext context) {
final MyInheritedWidgetState state = MyInheritedWidget.of(context, false);
return new Container(
child: new RaisedButton(
child: new Text('Add Item'),
onPressed: () {
state.addItem('new item');
},
),
);
}
}
複製代碼
在那裏,當咱們按下它時,Widget A再也不重建。
路由Routes,對話框Dialogs , BuildContexts與應用程序綁定。這意味着即便在屏幕A內部您要求顯示另外一個屏幕B(例如,在當前的屏幕上),兩個屏幕中的任何一個都沒有「簡單的方法」來關聯它們本身的上下文。屏幕B瞭解屏幕A上下文的惟一方法是從屏幕A獲取它做爲Navigator.of(context).push(...。)的參數。
推薦閱讀:
[1] : flutter屏幕適配
[2] : Maksim Ryzhikov
[3] : Chema Molins
[4] : Official documentation
[5] : Video from Google I/O 2018
[6] : Scoped_Model