上一節我們熟悉了初始化後的flutter的介面。這一節,我們就來重點了解一下這部分的內容。
StatelessWidgets and StatefulWidgets
- Flutter中的Widget都必須從Flutter庫中繼承。
你將使用的兩個幾乎總是
StatelessWidget
和StatefulWidget
。顧名思義,我們只要如果是不需要根據狀態變化的元件,我們可以直接繼承StatelessWidget
.如果和狀態有關係的元件就必須繼承StatefulWidget
。 - Flutter中的
Widget
都是不可變的狀態。 但是實際上,總要根據對應的狀態,檢視發生變化,所以就有了state
。用它來保持我們的狀態。 這樣,一個Stateful Widget,實際上是兩個類:狀態物件state
和Widget
組成的。 如下程式碼
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
@override
_MyHomePageState createState() => new _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
int _counter = 0;
void _incrementCounter() {
setState(() {
_counter++;
});
}
@override
Widget build(BuildContext context) {
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),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
複製程式碼
setState
&build
_MyHomePageState
繼承於State
.一方面需要管理自己的狀態_counter
,一方面需要build
來構造元件。 改變狀態後,需要通過setState
來重新構建widget
,就是會重新呼叫build
方法,來得到狀態同步。
最常見的Widget
接著先看看一些常用的元件,這些是隨時可用的小部件,開箱即用,你會非常滿意:
Text
- 用於簡單地在螢幕上顯示文字的小部件。Image
- 用於顯示影象。Icon
- 用於顯示Flutter的內建Material和Cupertino圖示。Container
- 在Flutter中,相當於div
。允許在其中進行新增填充,對齊,背景,力大小以及其他東西的載入。空的時候也會佔用0px的空間,這很方便。- TextInput - 處理使用者反饋。
Row
,Column
- 這些小部件顯示水平或垂直方向的子項列表。Stack
- 堆疊顯示一個孩子的列表。這個功能很像CSS中的'position'屬性。Scaffold
- 為應用提供基本的佈局結構。它可以輕鬆實現底部導航,appBars,後退按鈕等。
更多的可以看目錄。
**注意:**如果您熟悉基於元件的框架(如React或Vue),則可能不需要閱讀此內容。Widget
就是元件。
封裝元件
這樣的話,實際開發中,也是通過不斷對元件的封裝,來提高工作效率。 比如簡單的封裝一個原型的圖片元件(實際上,應該這個width和height都可以封裝進去的。)
class CircleImage extends StatelessWidget {
final String renderUrl;
CircleImage(this.renderUrl);
@override
Widget build(BuildContext context) {
return Container(
width: 100.0,
height: 100.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: new DecorationImage(
fit: BoxFit.cover,
image: NetworkImage(renderUrl ?? ''),
),
),
);
}
}
//直接使用
new CircleImage('https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533638174553&di=6913961a358faf638b6233e5d3dcc2b2&imgtype=0&src=http%3A%2F%2Fimage.9game.cn%2F2015%2F3%2F5%2F10301938.png')
複製程式碼
執行效果(中間皮卡丘)
Stateful Widget 的生命週期
現在讓我們深入一點,
先來思考一下
- 為什麼Stateful Widget會將State
和Widget
分開呢?
- 答案就只有一個:效能。
State
管理著狀態,它是常駐的。然而,Widget
是不可變的,當配置發生變化,它會立馬發生重建。所以這樣的重建的成本是極低的。 因為State
在每次重建時都沒有拋棄,所以可以維護它並且不必每次重建某些東西時都要進行昂貴的計算以獲得狀態屬性。- 此外,這是允許Flutter動畫存在的原因。因為
State
沒有丟棄,它可以不斷重建它的Widget以響應資料變化。
1. createState()
當建立一個StatefulWidget
時。立即呼叫。通常都是如下,這樣簡單的操作。
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => new _MyHomePageState();
}
複製程式碼
2. mounted is true
當這個Widget
呼叫createState
後, 會將buildContext
傳入。 BuildContext
內有自己在widget tree
上相關的資訊。
所有的widgets
都有 bool this.mounted
這個屬性. 當BuildContext
傳入時,它將會被標記成 true。如果這個屬性不是true的話,呼叫setState
會報錯。
注意:你可以在呼叫setState
前,檢查一下這個變數。
if (mounted) {...` to make sure the State exists before calling `setState()
複製程式碼
3 . initState()
這個方法只會呼叫一次,在這個Widget
被建立之後。它必須呼叫super.initState()
.
在這裡可以做:
- 初始化根據對應
BuildContext
的狀態 - 初始化根據在樹上的父節點的屬性確定的值
- 註冊
Streams
ChangeNotifiers
或者其他會改變的資料的監聽。
@override
initState() {
super.initState();
// Add listeners to this class
cartItemStream.listen((data) {
_updateWidget(data);
});
}
複製程式碼
4. didChangeDependencies()
它是在initState
方法後,就會呼叫。
當Widget
依賴的一些資料(比如說是InheritedWidget
,後面會介紹)更新時,它會立即被呼叫。
同時build
方法,會自動呼叫。
需要注意的是,你需要通過呼叫BuildContext.inheritFromWidgetOfExactType
,手動去註冊InheritedWidget
的監聽後,這個方法才會起作用。
文件還建議,當InheritedWidget更新時,如果需要進行網路呼叫(或任何其他昂貴的操作),它可能會很有用。
5.build()
這個方法會經常被呼叫。
6. didUpdateWidget(Widget oldWidget)
如果父元件發生變化,而且必須去重建widget時,而且被相同的runtimeType
重建時,這個方法會被呼叫。
因為Flutter是複用state
的。所以,你可能需要重新初始化狀態。
如果你的Widget
是需要根據監聽的資料,發生變化的,那麼你就需要從舊的物件中反註冊,然後註冊新的物件。
注意:如果您希望重建與此狀態關聯的Widget,則此方法基本上是'initState'的替代!
這個方法,會自動呼叫build
,所以不需要去呼叫setState
@override
void didUpdateWidget(Widget oldWidget) {
if (oldWidget.importantProperty != widget.importantProperty) {
_init();
}
}
複製程式碼
7. setState()
這個方法會被framework
和開發者不斷呼叫。用來通知元件重新整理。
這個方法的不能有非同步的回撥。其他,就可以隨便使用。
void updateProfile(String name) {
setState(() => this.name = name);
}
複製程式碼
8. deactivate()
(這個狀態暫時不是很理解)
State
從樹中刪除時會呼叫Deactivate
,但可能會在當前幀更改完成之前重新插入。此方法的存在主要是因為State
物件可以從樹中的一個點移動到另一個點。
這很少使用。
9. dispose()
State刪除物件時呼叫Dispose ,這是永久性的。 在此方法取消訂閱並取消所有動畫,流等
10. mounted is false
state
物件被移除了,如果呼叫setState
,會丟擲的錯誤。
一些疑問
BuildContext
- 1. 每個widget
都有自己的context
。這個context是父元件通過build方法給他返回的。
首先,先看下面程式碼。我們將在四個地方列印context的hashCode,來看看有什麼不同
//...
_MyHomePageState() {
//1. constructor
print('constructor context hashcode = ${context.hashCode}');
}
void _incrementCounter() {
//2. member method
print('_incrementCounter context hashcode = ${context.hashCode}');
setState(() {
_counter++;
});
}
@override
void initState() {
super.initState();
//3. initState
print('initState context hashcode = ${context.hashCode}');
}
@override
Widget build(BuildContext context) {
return new Scaffold(
//...
floatingActionButton: new FloatingActionButton(
onPressed: () {
//4.floattingbutton
print(
'FloatingActionButton onPressed context hashcode = ${context.hashCode}');
_incrementCounter();
},
tooltip: 'Increment',
child: new Icon(Icons.add),
), // This trailing comma makes auto-formatting nicer for build methods.
);
}
}
複製程式碼
很明顯可以看到,我們在initState
方法時,已經分配拿到了父元件的BuildContext.接下來的直接使用context,也都是同一個。
我們知道可以通過Scaffold的context來彈出一個SnackBar
。這裡想通過點選彈出這個。
修改程式碼如下:
//...
floatingActionButton: new FloatingActionButton(
onPressed: () {
print(
'FloatingActionButton onPressed context hashcode = ${context.hashCode}');
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('I am context from Scaffold'),
));
},
tooltip: 'Increment',
child: new Icon(Icons.add),
),
複製程式碼
執行,但是執行報錯資訊如下:
很明顯。通過上面的測試,我們知道這裡的context,確實不是Scaffold。那我們要如何在這裡拿到Scaffold的context呢?
2. 通過builder方法
修改程式碼如下,通過Builder方法,得到這個context
.
//...
floatingActionButton: new Builder(
builder: (context) {
return new FloatingActionButton(
onPressed: () {
print(
'FloatingActionButton onPressed context hashcode = ${context.hashCode}');
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('I am context from Scaffold'),
));
_incrementCounter();
},
tooltip: 'Increment',
child: new Icon(Icons.add),
);
},
)
複製程式碼
執行結果
我們可以看到,我們確實拿到了Scaffold
分配的Context
,而且彈出了SnackBar
.
後續過程中,一定要注意這個Context的使用。
注意:這裡其實還有另外一個方法,來得到這個BuildContext
。就是將FloatingActionButton分離出來,寫成另外一個元件,就能通過build
方法拿到了。
方法如下:
- 新增類
class ScaffoldButton extends StatelessWidget {
ScaffoldButton({this.onPressedButton});
final VoidCallback onPressedButton;
@override
Widget build(BuildContext context) {
return FloatingActionButton(
onPressed: () {
print(
'FloatingActionButton onPressed context hashcode = ${context.hashCode}');
Scaffold.of(context).showSnackBar(
SnackBar(content: Text('I am context from Scaffold')));
onPressedButton();
},
tooltip: 'Increment',
child: new Icon(Icons.add),
);
}
}
複製程式碼
再將floatingActionButton
修改成這個類
//...
floatingActionButton: ScaffoldButton(
onPressedButton: () {
_incrementCounter();
},
));
複製程式碼
不知所云的構造引數 Key
隨意點開一個Widget
,就會發現,可以傳遞一個引數Key
.那這個Key到底是幹啥子,有什麼用呢?
Flutter是受React啟發的,所以Virtual Dom的diff演算法也參考過來了(應該是略有修改),在diff的過程中如果節點有Key來比較的話,能夠最大程度重用已有的節點(特別在列表的場景),除了這一點這個Key也用在很多其他的地方這個以後會總結一下。總之,這裡我們可以知道key能夠提高效能,所以每個Widget都會構建方法都會有一個key的引數可選,貫穿著整個框架。
通常情況下,我們不需要去傳遞這個Key
。因為framework
會在內部自處理它,來區分不同的widgets
下面有幾種情況,我們可以使用它
- 使用ObjectKey
和ValueKey
來對元件進行區分。
可以看PageStorageKey, 和另外一個例子,這個例子是deletion: flutter.io/cookbook/ge….
簡單的來說,當我們使用Row
或者Column
時,想要執行一個remove
的動畫
new AnimatedList(
children: [
new Card(child: new Text("foo")),
new Card(child: new Text("bar")),
new Card(child: new Text("42")),
]
)
複製程式碼
當我們移除"bar"後
new AnimatedList(
children: [
new Card(child: new Text("foo")),
new Card(child: new Text("42")),
]
)
複製程式碼
因為我們沒有定義Key,所以可能flutter並不知道,我們那個item發生了改變,所以可能發生在位置1上的動畫,可能發生在其他位置。 正確的修改如下:
new AnimatedList(
children: [
new Card(key: new ObjectKey("foo"), child: new Text("foo")),
new Card(key: new ObjectKey("bar"), child: new Text("bar")),
new Card(key: new ObjectKey("42"), child: new Text("42")),
]
)
複製程式碼
這樣當我們移除"bar"的時候,flutter就能準確的區別到正確的位置上。
Key
雖然不是Index
,但是對於每一個元素來說,是獨一無二的。
- 使用GlobalKey
- 使用
GlobalKey
的場景是,從父控制元件和跨子Widget
來傳遞狀態時。 需要注意的是:不要濫用GlobalKey,如果有更好的方式的,請使用其他方式來傳遞狀態。
這裡有一個例子是 通過給Scaffold新增GolbalKey。然後通widget.GolbalKey.state來呼叫showSnackBar
class _MyHomePageState extends State<MyHomePage> {
final globalKey =
new GlobalKey<ScaffoldState>();
void _incrementCounter() {
globalKey.currentState
.showSnackBar(SnackBar(content: Text('I am context from Scaffold')));
}
@override
Widget build(BuildContext context) {
return new Scaffold(
key: globalKey,
//...
)
}
}
複製程式碼
這樣就可以直接從父控制元件呼叫子Widget
的狀態。
- 還有一個場景是,過渡動畫,當兩個頁面都是相同的Widget時,也可以使用GlobalKey。
總結
這邊文章,我們對StateFulWidget有了升入的認識。
- 認識了通用的控制元件
- 瞭解了StatefulWidget的生命週期
- 對BuildContext 瞭解。
- 對Key的場景進行了瞭解。得到了使用GlobalKey來跨子元件傳遞狀態的方式。
下一遍文章:我們將更加深入的對Flutter的介面開發的一些原理
參考文章
Flutter中的Key,LocalKey,GlobalKey... And More
what-are-keys-used-for-in-flutter-framework