本文介紹了Flutter應用程式中Widget,State,BuildContext和InheritedWidget的重要概念。
特別注意InheritedWidget,它是最重要且記錄較少的小部件之一。
本文內容很長,但做技術就是要沉得下心!
難度:初學者
前言
Flutter中Widget,State和BuildContext的概念是每個Flutter開發人員需要完全理解的最重要概念之一。 但是,文件很龐大,並不總是清楚地解釋這個概念。
我會用自己的話語和捷徑來解釋這些概念,本文的真正目的是試圖澄清以下主題:
- 有狀態和無狀態小部件之間的區別
- 什麼是BuildContext
- 什麼是State以及如何使用它
- BuildContext與其State物件之間的關係
- InheritedWidget以及在Widgets樹中傳播資訊的方式
- 重建的概念
第1部分:概念
小工具的概念
在Flutter中,幾乎所有東西都是Widget。
將Widget視為可視元件(或與應用程式的可視方面互動的元件)。
當您需要構建與佈局直接或間接相關的任何內容時,您正在使用視窗小部件。
小部件樹的概念
視窗小部件以樹形結構組織。
包含其他小部件的小部件稱為父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只不過是對構建的所有視窗小部件的樹結構中的視窗小部件的位置的引用。
簡而言之,將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,但不建議這樣做(我們稍後會討論)
小部件的型別
小部件有兩種型別:
無狀態小工具 Stateless 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
。
這種無狀態小部件的生命週期很簡單:
- 初始化
- 通過build()渲染
有狀態的小工具 Stateful Widget
其他一些小部件將處理一些在Widget生命週期內會發生變化的內部資料。因此,該資料變得動態。
此Widget儲存的資料集可能會在此Widget的生命週期內發生變化,稱為State。
這些視窗小部件稱為有狀態視窗小部件(Stateful Widget)。
這樣的Widget的示例可以是使用者可以選擇的核取方塊列表或者根據條件禁用的Button。
State的概念
State定義StatefulWidget例項的“行為”部分。 它包含旨在與Widget互動/干擾的資訊:
- 行為
- 佈局
應用於狀態的任何更改都會強制Widget重建。
State和BuildContext之間的關係
對於有狀態視窗小部件,狀態與BuildContext關聯。
此關聯是永久性的 ,State物件永遠不會更改其BuildContext。 即使可以在樹結構周圍移動Widget BuildContext,State仍將與該BuildContext保持關聯。
當State與BuildContext關聯時,State被視為已掛載。
重點: 由於State物件與BuildContext相關聯,這意味著State物件不能(直接)通過另一個BuildContext訪問!(我們將在稍後討論這個問題)。
有狀態的小部件Stateful Widget 的生命週期
這是與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()
initState()方法是建立State物件後要呼叫的第一個方法(在建構函式之後)。
需要執行其他初始化時,將覆蓋重寫此方法。典型的初始化與動畫,控制器有關...... 如果重寫此方法,則需要在第一個位置呼叫super.initState()方法。
在這個方法中,上下文context
可用,但你還不能真正使用它,因為框架還沒有完全將狀態與它相關聯。
initState()方法完成後,State物件現在已初始化,上下文可用。
在此State物件的生命週期內不再呼叫此方法。
didChangeDependencies()
didChangeDependencies()
方法是要呼叫的第二個方法。
在此階段,由於上下文可用,您可以使用它。
如果您的Widget連結到InheritedWidget和/或您需要初始化一些偵聽器(基於BuildContext),則通常會覆蓋此方法。
請注意,如果您的視窗小部件連結到InheritedWidget,則每次重建此視窗小部件時都會呼叫此方法。
如果重寫此方法,則應首先呼叫super.didChangeDependencies()
。
build()
build(BuildContext context)
方法在didChangeDependencies()
(和didUpdateWidget
)之後呼叫。
這是您構建視窗小部件(可能還有任何子樹)的位置。
每次State物件更改時(或者當InheritedWidget需要通知“已註冊”的小部件時)都會呼叫此方法!
為了強制重建,您可以呼叫setState((){...})
方法。
dispose()
放棄視窗小部件時呼叫dispose()方法。
如果你需要執行一些清理(例如監聽器,控制器......),然後立即呼叫super.dispose()
,則覆蓋此方法。
選擇無狀態還是有狀態小部件?
這是許多開發人員需要問自己的問題:“我是否需要我的Widget無狀態或有狀態?” 為了回答這個問題,請問自己:
在我的小部件的生命週期中,我是否需要考慮一個將要更改的變數,何時更改,將強制重建小部件?
如果問題的答案是肯定的,那麼您需要一個有狀態的小部件,否則,您需要一個無狀態小部件。 一些例子:
- 用於顯示覆選框列表的小元件。要顯示覆選框,您需要考慮一系列專案。每個專案都是一個具有標題和狀態的物件。如果單擊核取方塊,則切換相應的
item.status
;在這種情況下,您需要使用有狀態視窗小部件來記住專案的狀態,以便能夠重繪核取方塊。 - 帶有表格的螢幕。該螢幕允許使用者填寫表單的視窗小部件並將表單傳送到伺服器。在這種情況下,在這種情況下,除非您在提交表單之前需要驗證表單或執行任何其他操作,否則無狀態視窗小部件可能就足夠了。
Stateful Widget由2部分組成
還記得Stateful小部件的結構嗎?有兩個部分:
Widget的主要定義
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的第一部分定義的任何變數通常在其生命週期內不會更改。
Widget State定義
class _MyStatefulWidgetState extends State<MyStatefulWidget> {
...
@override
Widget build(BuildContext context){
...
}
}
複製程式碼
第二部分“_MyStatefulWidgetState”是在Widget的生命週期中變化的部分,並強制每次應用修改時重建Widget的這個特定例項。
名稱以_
開頭的字元使其成為.dart檔案的私有。
如果需要在.dart檔案之外引用此類,請不要使用“_”字首。
_MyStatefulWidgetState類可以訪問儲存在MyStatefulWidget中的任何變數,使用widget.{變數的名稱}
。
例如:widget.color
小部件唯一標識 - key
在Flutter中,每個Widget都是唯一標識的。這個唯一標識由構建/渲染時的框架定義。
此唯一標識對應於可選的Key引數。如果省略,Flutter將為您生成一個。
在某些情況下,您可能需要強制使用此金鑰,以便可以通過其金鑰訪問視窗小部件。
為此,您可以使用以下幫助程式之一:GlobalKey ,LocalKey,UniqueKey 或ObjectKey。
該GlobalKey確保關鍵是在整個應用程式唯一的。 強制使用Widget的唯一標識:
GlobalKey myKey = new GlobalKey();
...
@override
Widget build(BuildContext context){
return new MyWidget(
key: myKey
);
}
複製程式碼
第2部分:如何進入State?
如前所述,State連結到一個BuildContext,BuildContext連結到Widget的一個例項。
1. Widget本身
理論上,唯一能夠訪問狀態的是Widget State本身。
在這種情況下,沒有困難。Widget State類訪問其任何變數。
2. 一個直接的 child Widget
有時,父視窗小部件可能需要訪問其直接子節點的狀態才能執行特定任務。 在這種情況下,要訪問這些直接子項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...'),
)
);
}
),
),
);
}
}
複製程式碼
3. 祖先Widget
假設您有一個屬於另一個Widget的子樹的Widget,如下圖所示。
為了實現這一目標,需要滿足3個條件:
1.“帶狀態的小工具”(紅色)需要暴露其State
為了公開它的狀態,Widget需要在建立時記錄它,如下所示:
class MyExposingWidget extends StatefulWidget {
MyExposingWidgetState myState;
@override
MyExposingWidgetState createState(){
myState = new MyExposingWidgetState();
return myState;
}
}
複製程式碼
2.“Widget State”需要暴露一些getter / setter 為了讓“stranger”設定/獲取狀態屬性,Widget State需要通過以下方式授權訪問:
- 公共屬性 (不推薦)
- getter / setter
例如:
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允許在視窗小部件樹中有效地傳播(和共享)資訊。
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的資料?
在構建子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,
);
}
}
複製程式碼
如何在小部件之間進行互動?
請考慮以下顯示視窗小部件樹結構的圖表。
為了說明一種互動方式,我們假設如下:
- '小部件A'是一個將專案新增到購物車的按鈕;
- “小部件B”是一個顯示購物車中商品數量的文字;
- “小部件C”位於小部件B旁邊,是一個內部帶有任何文字的文字;
- 我們希望“Widget B”在按下“Widget A”時自動在購物車中顯示正確數量的專案,但我們不希望重建“Widget C”
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”按鈕新增一個Item時都會重新建立它
- MyInheritedWidget是一個Widget,其狀態包含Items列表。可以通過“(BuildContext context)的靜態MyInheritedWidgetState”訪問此狀態。
- MyInheritedWidgetState公開一個getter(itemsCount)和一個方法(addItem),以便它們可以被小部件使用,這是子小部件樹的一部分
- 每次我們向State新增一個Item時,MyInheritedWidgetState都會重建
- MyTree類只是構建一個小部件樹,將MyInheritedWidget作為樹的父級
- WidgetA是一個簡單的RaisedButton,當按下它時,從最近的MyInheritedWidget呼叫addItem方法
- WidgetB是一個簡單的文字,顯示最接近的MyInheritedWidget級別的專案數
這一切如何運作? 註冊Widget以供以後通知
當子Widget呼叫MyInheritedWidget.of(context)時,它會呼叫MyInheritedWidget的以下方法,並傳遞自己的BuildContext。
static MyInheritedWidgetState of(BuildContext context) {
return (context.inheritFromWidgetOfExactType(_MyInherited) as _MyInherited).data;
}
複製程式碼
在內部,除了簡單地返回MyInheritedWidgetState的例項之外,它還將消費者視窗小部件訂閱到更改通知。
在場景後面,對這個靜態方法的簡單呼叫實際上做了兩件事:
- 當對InheritedWidget應用修改時,“consumer”視窗小部件會自動新增到將重建的訂戶列表中(此處為_MyInherited)
- _MyInherited小部件(又名MyInheritedWidgetState)中引用的資料將返回給“使用者”
過程
由於'Widget A'和'Widget B'都已使用InheritedWidget訂閱,因此如果對_MyInherited應用了修改,則當單擊Widget A的RaisedButton時,操作流程如下(簡化版本):
- 呼叫MyInheritedWidgetState的addItem方法
- MyInheritedWidgetState.addItem方法將新項新增到List
- 呼叫setState()以重建MyInheritedWidget
- 使用List 的新內容建立_MyInherited的新例項
- _MyInherited記錄在引數(資料)中傳遞的新State作為InheritedWidget,它檢查是否需要“通知”“使用者”(答案為是)
- 它迭代整個消費者列表(這裡是Widget A和Widget B)並請求他們重建
- 由於Wiget C不是消費者,因此不會重建。
嗯,就是這麼幹的 !
但是,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;
}
複製程式碼
通過新增布林型別的額外引數...
- 如果“rebuild”引數為true(預設情況下),我們使用常規方法(並且Widget將新增到訂閱者列表中)
- 如果“rebuild”引數為false,我們仍然可以訪問資料,但不使用InheritedWidget的內部實現
因此,要完成解決方案,我們還需要稍微更新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的特別說明......
路由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