- 原文地址:Widget - State - Context - InheritedWidget
- 原文做者:www.didierboelens.com
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:nanjingboy
- 校對者:Mirosalva、HearFishle
本文涵蓋了 Flutter 應用中有關 Widget、State、Context 及 InheritedWidget 的重要概念。由於 InheritedWidget 是最重要且文檔缺少的部件之一,故需特別關注。html
難度:初學者前端
Flutter 中的 Widget、State 及 Context 是每一個 Flutter 開發者都須要充分理解的最重要的概念之一。react
雖然存在大量文檔,但並無一個可以清晰地解釋它。android
我將用本身的語言來解釋這些概念,知道這些可能會讓一些純理論者感到不安,但本文的真正目的是試圖說清如下主題:ios
本文同時發佈於 Medium - Flutter Community。git
在 Flutter 中,幾乎全部的東西都是 Widget。github
將一個 Widget 想象爲一個可視化組件(或與應用可視化方面交互的組件)。後端
當你須要構建與佈局直接或間接相關的任何內容時,你正在使用 Widget。服務器
Widget 以樹結構進行組織。markdown
包含其餘 Widget 的 Widget 被稱爲父 Widget(或Widget 容器)。包含在父 Widget 中的 Widget 被稱爲子 Widget。
讓咱們用 Flutter 自動生成的基礎應用來講明這一點。如下是簡化代碼,僅有 build 方法:
@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),
),
);
}
複製代碼
若是咱們如今觀察這個基本示例,咱們將得到如下 Widget 樹結構(限制代碼中存在的 Widget 列表):
另一個重要的概念是 Context。
Context 僅僅是已建立的全部 Widget 樹結構中某個 Widget 的位置引用。
簡而言之,將 context 做爲 Widget 樹的一部分,其中 context 所對應的 Widget 被添加到此樹中。
一個 context 僅僅從屬於一個 widget。
若是 widget ‘A’ 擁有子 widget,那麼 widget ‘A’ 的 context 將成爲其直接關聯子 context 的父 context。
讀到這裏會很明顯發現 context 是連接在一塊兒的,而且會造成一個 context 樹(父子關係)。
若是咱們如今嘗試在上圖中說明 Context 的概念,咱們獲得(依舊是一個很是簡化的視圖)每種顏色表明一個 context(除了 MyApp,它是不一樣的):
Context 可見性 (簡短描述):
某些東西 只能在本身的 context 或在其父 context 中可見。
經過上述描述咱們能夠將其從子 context 中提取出來,它很容易找到一個 祖先(= 父)Widget。
一個例子,考慮 Scaffold > Center > Column > Text:context.ancestorWidgetOfExactType(Scaffold) => 經過從 Text 的 context 獲得樹結構來返回第一個 Scaffold。
從父 context 中,也能夠找到 後代(= 子)Widget,但不建議這樣作(咱們將稍後討論)。
Widget 擁有 2 種類型:
這些可視化組件除了它們自身的配置信息外不依賴於任何其餘信息,該信息在其直接父節點構建時提供。
換句話說,這些 Widget 一旦建立就不關心任何變化。
這樣的 Widget 稱爲 Stateless Widget。
這種 Widget 的典型示例能夠是 Text、Row、Column 和 Container 等。在構建時,咱們只需將一些參數傳遞給它們。
參數能夠是裝飾、尺寸、甚至其餘 widget 中的任何內容。須要強調的是,該配置一旦被建立,在下次構建過程以前都不會改變。
stateless widget 只有在 loaded/built 時纔會繪製一次,這意味着任何事件或用戶操做都沒法對該 Widget 進行重繪。
如下是與 Stateless Widget 相關的典型代碼結構。
以下所示,咱們能夠將一些額外的參數傳遞給它的構造函數。但請記住,這些參數在後續階段將不改變(變化),而且必須按照已有狀態使用。
class MyStatelessWidget extends StatelessWidget {
MyStatelessWidget({
Key key,
this.parameter,
}): super(key:key);
final parameter;
@override
Widget build(BuildContext context){
return new ...
}
}
複製代碼
即便有另外一個方法能夠被重寫(createElement),後者也幾乎不會被重寫。惟一須要被重寫的是 build 方法。
這種 Stateless Widget 的生命週期是至關簡單的:
其餘一些 Widget 將處理一些在 Widget 生命週期內會發生變化的內部數據。所以,此類數據會變爲動態。
該 Widget 所持有的數據集在其生命週期內可能會發生變化,這樣的數據被稱爲 State。
這些 Widget 被稱爲 Stateful Widget。
此類 Widget 的示例多是用戶可選擇的複選框列表,也能夠是根據條件禁用的 Button 按鈕。
State 定義了 StatefulWidget 實例的 「行爲」。
它包含了用於 交互 / 干預 Widget 信息:
應用於 State 的任何更改都會強制 Widget 進行重建。
對於 Stateful Widget,State 與 Context 相關聯。而且此關聯是永久性的,State 對象將永遠不會改變其 context。
即便能夠在樹結構周圍移動 Widget Context,State 仍將與該 context 相關聯。
當 State 與 Context 關聯時,State 被視爲已掛載。
重點:
State 對象 與 context 相關聯,就意味着該 State 對象是不(直接)訪問另外一個 context!(咱們將在稍後討論該問題)。
既然已經介紹了基本概念,如今是時候更加深刻一點了……
如下是與 Stateful Widget 相關的典型代碼結構。
因爲本文的主要目的是用「變量」數據來解釋 State 的概念,我將故意跳過任何與 Stateful Widget 相關的一些可重寫方法的解釋,這些方法與此沒有特別的關係。這些可重寫的方法是 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 ...
}
}
複製代碼
下圖展現了與建立 Stateful Widget 相關的操做/調用序列(簡化版本)。在圖的右側,你將注意到數據流中 State 對象的內部狀態。你還將看到此時 context 與 state 已經關聯,而且 context 所以變爲可用狀態(mounted)。
接下來讓咱們經過一些額外的細節來解釋它:
一旦 State 對象被建立,initState() 方法是第一個(構造函數以後)被調用的方法。當你須要執行額外的初始化時,該方法將會被重寫。常見的初始化與動畫、控制器等相關。若是重寫該方法,你應該首先調用 super.initState()。
該方法能夠獲得 context,但沒法真正使用它,由於框架尚未徹底將其與 state 關聯。
一旦 initState() 方法執行完成,State 對象就被初始化而且 context 變爲可用。
在該 State 對象的生命週期內將不會再次調用此方法。
didChangeDependencies() 方法是第二個被調用的方法。
在這一階段,因爲 context 是可用的,因此你可使用它。
若是你的 Widget 連接到了一個 InheritedWidget 而且/或者你須要初始化一些 listeners(基於 context),一般會重寫該方法。
請注意,若是你的 widget 連接到了一個 InheritedWidget,在每次重建該 Widget 時都會調用該方法。
若是你重寫該方法,你應該首先調用 super.didChangeDependencies()。
build(BuildContext context) 方法在 didChangeDependencies()(及 didUpdateWidget)以後被調用。
這是你構建你的 widget(可能還有任何子樹)的地方。
每次 State 對象更新(或當 InheritedWidget 須要通知「已註冊」 widget)時都會調用該方法!!
爲了強制重建,你可能須要調用 setState((){…}) 方法。
dispose() 方法在 widget 被廢棄時被調用。
若是你須要執行一些清理操做(好比:listeners),則重寫該方法,並在此以後當即調用 super.dispose()。
這是許多開發者都須要問本身的問題:我是否須要 Widget 爲 Stateless 或 Stateful?
爲了回答這個問題,請問問本身:
在個人 widget 生命週期中,是否須要考慮一個將要變動,而且在變動後 widget 將強制重建的變量?
若是問題的答案是 yes,那麼你須要一個 Stateful Widget,不然,你須要一個 Stateless Widget。
一些例子:
用於顯示覆選框列表的 widget。要顯示覆選框,你須要考慮一系列項目。每一個項目都是一個包含標題和狀態的對象。若是你點擊一個複選框,相應的 item.status 將會切換;
在這種狀況下,你須要使用一個 Stateful Widget 來記住項目的狀態,以便可以重繪複選框。
帶有表格的屏幕。該屏幕容許用戶填寫表單的 Widget 並將表單發送到服務器。
在這種狀況下,除非你要對錶單進行驗證,或在提交以前作一些其餘的事情,一個 Stateless Widget 可能就足夠了。
還記得 Stateful widget 的結構嗎?有 2 個部分:
class MyStatefulWidget extends StatefulWidget {
MyStatefulWidget({
Key key,
this.color,
}): super(key: key);
final Color color;
@override
_MyStatefulWidgetState createState() => new _MyStatefulWidgetState();
}
複製代碼
第一部分 「MyStatefulWidget」 一般是 Widget 的 public 部分。當你須要將其添加到 widget 樹時,能夠實例化它。該部分在 Widget 生命週期內不會發生變化,但可能接受與其相關的 State 實例化時使用的參數。
請注意,在 Widget 第一部分定義的任何變量一般在其生命週期內不會發生變化。
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
...
@override
Widget build(BuildContext context){
...
}
}
複製代碼
第二部分 「_MyStatefulWidgetStat」 管理 Widget 生命週期中的變化,並強制每次應用修改時重建該 Widget 實例。名稱開頭的 ‘_’ 字符使得該類對 .dart 文件是私有的。
若是你須要在 .dart 文件以外引用此類,請不要使用 ‘_’ 前綴。
_MyStatefulWidgetState
類能夠經過使用 widget.{變量名稱} 來訪問被存儲在 MyStatefulWidget 中的任何變量。在該示例中爲:widget.color。
在 Fultter 中,每個 Widget 都是被惟一標識的。這個惟一標識在 build/rendering 階段由框架定義。
該惟一標識對應於可選的 Key 參數。若是省略該參數,Flutter 將會爲你生成一個。
在某些狀況下,你可能須要強制使用此 key,以即可以經過其 key 訪問 widget。
爲此,你可使用如下方法中的任何一個:GlobalKey、LocalKey、UniqueKey 或 ObjectKey。
GlobalKey 確保生成的 key 在整個應用中是惟一的。
強制 Widget 使用惟一標識:
GlobalKey myKey = new GlobalKey();
...
@override
Widget build(BuildContext context){
return new MyWidget(
key: myKey
);
}
複製代碼
如前所述,State 被連接到 一個 Context,而且一個 Context 被連接到一個 Widget 實例。
從理論上講,惟一可以訪問 State 的是 Widget State 自身。
在此中狀況下不存在任何困難。Widget State 類能夠訪問任何內部變量。
有時,父 widget 可能須要訪問其直接子節點的 State 才能執行特定任務。
在這種狀況下,要訪問這些直接子節點的 State,你須要瞭解它們。
呼叫某人的最簡單方法是經過名字。在 Flutter 中,每一個 Widget 都有一個惟一的標識,由框架在 build/rendering 時肯定。如前所示,你可使用 key 參數爲 Widget 強制指定一個標識。
...
GlobalKey<MyStatefulWidgetState> myWidgetStateKey = new GlobalKey<MyStatefulWidgetState>();
...
@override
Widget build(BuildContext context){
return new MyStatefulWidget(
key: myWidgetStateKey,
color: Colors.blue,
);
}
複製代碼
一經肯定,父 Widget 能夠經過如下形式訪問其子節點的 State:
myWidgetStateKey.currentState
讓咱們考慮當用戶點擊按鈕時顯示 SnackBar 這樣一個基本示例。因爲 SnackBar 是 Scaffold 的子 Widget,它不能被 Scaffold 內部任何其餘子節點直接訪問(還記得 context 的概念以及其層次/樹結構嗎?)。所以,訪問它的惟一方法是經過 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 個條件:
爲了暴露 其 State,Widget 須要在建立時記錄它,以下所示:
class MyExposingWidget extends StatefulWidget {
MyExposingWidgetState myState;
@override
MyExposingWidgetState createState(){
myState = new MyExposingWidgetState();
return myState;
}
}
複製代碼
爲了讓「其餘類」 設置/獲取 State 中的屬性,Widget State 須要經過如下方式受權訪問:
例子:
class MyExposingWidgetState extends State<MyExposingWidget>{
Color _color;
Color get color => _color;
...
}
複製代碼
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,
);
}
}
複製代碼
這個解決方案很容易實現,但子 widget 如何知道它什麼時候須要重建呢?
經過此方案,它無能爲力。它必須等到重建發生後才能刷新其內容,此方法不是特別方便。
下一節將討論 Inherited Widget 的概念,它能夠解決這個問題。
簡而言之,InheritedWidget 容許在 widget 樹中有效地向下傳播(和共享)信息。
InheritedWidget 是一個特殊的 Widget,它將做爲另外一個子樹的父節點放置在 Widget 樹中。該子樹的全部 widget 都必須可以與該 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,目的在於爲子樹中的全部 widget 提供某些『共享』數據。
如前所述,爲了可以傳播/共享某些數據,須要將 InheritedWidget 放置在 widget 樹的頂部,這解釋了傳遞給 InheritedWidget 基礎構造函數的 @required Widget child 參數。
static MyInheritedWidget of(BuildContext context) 方法容許全部子 widget 經過包含的 context 得到最近的 MyInheritedWidget 實例(參見後面的內容)。
最後重寫 updateShouldNotify 方法用來告訴 InheritedWidget 若是對數據進行了修改,是否必須將通知傳遞給全部子 widget(已註冊/已訂閱)(請參考下文)。
所以,咱們須要將它放在樹節點級別,以下所示:
class MyParentWidget... {
...
@override
Widget build(BuildContext context){
return new MyInheritedWidget(
data: counter,
child: new Row(
children: <Widget>[
...
],
),
);
}
}
複製代碼
在構建子節點時,後者將得到 InheritedWidget 的引用,以下所示:
class MyChildWidget... {
...
@override
Widget build(BuildContext context){
final MyInheritedWidget inheritedWidget = MyInheritedWidget.of(context);
///
/// 此刻,該 widget 可以使用 MyInheritedWidget 暴露的數據
/// 經過調用:inheritedWidget.data
///
return new Container(
color: inheritedWidget.data.color,
);
}
}
複製代碼
請思考下圖中所顯示的 widget 樹結構。
爲了說明交互方式,咱們作如下假設:
針對該場景,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');
}
}
複製代碼
在這個很是基本的例子中:
_MyInherited
是一個 InheritedWidget,每次咱們經過 ‘Widget A’ 按鈕添加一個項目時它都會從新建立這一切是如何運做的呢?
當一個子 Widget 調用 MyInheritedWidget.of(context) 時,它傳遞自身的 context 並調用 MyInheritedWidget 的如下方法。
static MyInheritedWidgetState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
複製代碼
在內部,除了簡單地返回 MyInheritedWidgetState 實例外,它還訂閱消費者 widget 以便用於通知更改。
在幕後,對這個靜態方法的簡單調用實際上作了 2 件事:
_MyInherited
)應用修改時,該 widget 可以重建_MyInherited
widget(又名 MyInheritedWidgetState)中引用的數據將返回給消費者因爲 ‘Widget A’ 和 ‘Widget B’ 都使用 InheritedWidget 進行了訂閱,所以若是對 _MyInherited
應用了修改,那麼當點擊 Widget A 的 RaisedButton 時,操做流程以下(簡化版本):
_MyInherited
新的實例_MyInherited
記錄經過參數(data)傳遞的新 State至此它可以有效工做!
然而,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;
}
複製代碼
經過添加一個 boolean 類型的額外參數……
所以,要完成此方案,咱們還須要稍微修改一下 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 的 context 與 Application 綁定。
這意味着即便在屏幕 A 內部你要求顯示另外一個屏幕 B(例如,在當前的屏幕上),也沒法輕鬆地從兩個屏幕中的任何一個關聯它們本身的 context。
屏幕 B 想要了解屏幕 A 的 context 的惟一方法是經過屏幕 A 獲得它並將其做爲參數傳遞給 Navigator.of(context).push(….)
關於這些主題還有不少話要說……,特別是在 InheritedWidget 上。
在下一篇文章中我將介紹 Notifiers / Listeners 的概念,它們使用 State 和數據傳遞的方式上一樣很是有趣。
因此,請保持關注和快樂編碼。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。