上一節(jié)我們熟悉了初始化后的flutter的界面殿漠。這一節(jié)仇祭,我們就來(lái)重點(diǎn)了解一下這部分的內(nèi)容。
StatelessWidgets and StatefulWidgets
- Flutter中的Widget都必須從Flutter庫(kù)中繼承。
你將使用的兩個(gè)幾乎總是StatelessWidget
和StatefulWidget
搔确。顧名思義竟秫,我們只要如果是不需要根據(jù)狀態(tài)變化的組件娃惯,我們可以直接繼承StatelessWidget
.如果和狀態(tài)有關(guān)系的組件就必須繼承StatefulWidget
。 - Flutter中的
Widget
都是不可變的狀態(tài)肥败。
但是實(shí)際上趾浅,總要根據(jù)對(duì)應(yīng)的狀態(tài),視圖發(fā)生變化馒稍,所以就有了state
皿哨。用它來(lái)保持我們的狀態(tài)。
這樣纽谒,一個(gè)Stateful Widget证膨,實(shí)際上是兩個(gè)類:狀態(tài)對(duì)象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
.一方面需要管理自己的狀態(tài)_counter
鼓黔,一方面需要build
來(lái)構(gòu)造組件央勒。
改變狀態(tài)后,需要通過(guò)setState
來(lái)重新構(gòu)建widget
澳化,就是會(huì)重新調(diào)用build
方法崔步,來(lái)得到狀態(tài)同步。
最常見的Widget
接著先看看一些常用的組件缎谷,這些是隨時(shí)可用的小部件井濒,開箱即用,你會(huì)非常滿意:
-
Text
- 用于簡(jiǎn)單地在屏幕上顯示文本的小部件。 -
Image
- 用于顯示圖像眼虱。 -
Icon
- 用于顯示Flutter的內(nèi)置Material和Cupertino圖標(biāo)喻奥。 -
Container
- 在Flutter中,相當(dāng)于div
捏悬。允許在其中進(jìn)行添加填充撞蚕,對(duì)齊,背景过牙,力大小以及其他東西的加載甥厦。空的時(shí)候也會(huì)占用0px的空間寇钉,這很方便刀疙。 - TextInput - 處理用戶反饋。
-
Row
,Column
- 這些小部件顯示水平或垂直方向的子項(xiàng)列表扫倡。 -
Stack
- 堆棧顯示一個(gè)孩子的列表谦秧。這個(gè)功能很像CSS中的'position'屬性。 -
Scaffold
- 為應(yīng)用提供基本的布局結(jié)構(gòu)撵溃。它可以輕松實(shí)現(xiàn)底部導(dǎo)航疚鲤,appBars,后退按鈕等缘挑。
更多的可以看目錄集歇。
注意:如果您熟悉基于組件的框架(如React或Vue),則可能不需要閱讀此內(nèi)容语淘。Widget
就是組件诲宇。
封裝組件
這樣的話,實(shí)際開發(fā)中惶翻,也是通過(guò)不斷對(duì)組件的封裝姑蓝,來(lái)提高工作效率。
比如簡(jiǎn)單的封裝一個(gè)原型的圖片組件(實(shí)際上维贺,應(yīng)該這個(gè)width和height都可以封裝進(jìn)去的它掂。)
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 的生命周期
現(xiàn)在讓我們深入一點(diǎn)溯泣,
先來(lái)思考一下
- 為什么Stateful Widget會(huì)將State
和Widget
分開呢虐秋?
- 答案就只有一個(gè):性能。
-
State
管理著狀態(tài)垃沦,它是常駐的客给。然而,Widget
是不可變的肢簿,當(dāng)配置發(fā)生變化靶剑,它會(huì)立馬發(fā)生重建蜻拨。所以這樣的重建的成本是極低的。
因?yàn)?code>State在每次重建時(shí)都沒有拋棄桩引,所以可以維護(hù)它并且不必每次重建某些東西時(shí)都要進(jìn)行昂貴的計(jì)算以獲得狀態(tài)屬性缎讼。 - 此外,這是允許Flutter動(dòng)畫存在的原因坑匠。因?yàn)?code>State沒有丟棄血崭,它可以不斷重建它的Widget以響應(yīng)數(shù)據(jù)變化。
1. createState()
當(dāng)創(chuàng)建一個(gè)StatefulWidget
時(shí)厘灼。立即調(diào)用夹纫。通常都是如下,這樣簡(jiǎn)單的操作设凹。
class MyHomePage extends StatefulWidget {
@override
_MyHomePageState createState() => new _MyHomePageState();
}
2. mounted is true
當(dāng)這個(gè)Widget
調(diào)用createState
后, 會(huì)將buildContext
傳入舰讹。 BuildContext
內(nèi)有自己在widget tree
上相關(guān)的信息。
所有的widgets
都有 bool this.mounted
這個(gè)屬性. 當(dāng)BuildContext
傳入時(shí)闪朱,它將會(huì)被標(biāo)記成 true月匣。如果這個(gè)屬性不是true的話,調(diào)用setState
會(huì)報(bào)錯(cuò)监透。
注意:你可以在調(diào)用setState
前桶错,檢查一下這個(gè)變量。
if (mounted) {...` to make sure the State exists before calling `setState()
3 . initState()
這個(gè)方法只會(huì)調(diào)用一次胀蛮,在這個(gè)Widget
被創(chuàng)建之后。它必須調(diào)用super.initState()
.
在這里可以做:
- 初始化根據(jù)對(duì)應(yīng)
BuildContext
的狀態(tài) - 初始化根據(jù)在樹上的父節(jié)點(diǎn)的屬性確定的值
- 注冊(cè)
Streams
ChangeNotifiers
或者其他會(huì)改變的數(shù)據(jù)的監(jiān)聽糯钙。
@override
initState() {
super.initState();
// Add listeners to this class
cartItemStream.listen((data) {
_updateWidget(data);
});
}
4. didChangeDependencies()
它是在initState
方法后粪狼,就會(huì)調(diào)用。
當(dāng)Widget
依賴的一些數(shù)據(jù)(比如說(shuō)是InheritedWidget
,后面會(huì)介紹)更新時(shí)任岸,它會(huì)立即被調(diào)用再榄。
同時(shí)build
方法,會(huì)自動(dòng)調(diào)用享潜。
需要注意的是困鸥,你需要通過(guò)調(diào)用BuildContext.inheritFromWidgetOfExactType
,手動(dòng)去注冊(cè)InheritedWidget
的監(jiān)聽后剑按,這個(gè)方法才會(huì)起作用疾就。
文檔還建議,當(dāng)InheritedWidget更新時(shí)艺蝴,如果需要進(jìn)行網(wǎng)絡(luò)調(diào)用(或任何其他昂貴的操作)猬腰,它可能會(huì)很有用。
5.build()
這個(gè)方法會(huì)經(jīng)常被調(diào)用猜敢。
6. didUpdateWidget(Widget oldWidget)
如果父組件發(fā)生變化姑荷,而且必須去重建widget時(shí)盒延,而且被相同的runtimeType
重建時(shí),這個(gè)方法會(huì)被調(diào)用鼠冕。
因?yàn)镕lutter是復(fù)用state
的添寺。所以,你可能需要重新初始化狀態(tài)懈费。
如果你的Widget
是需要根據(jù)監(jiān)聽的數(shù)據(jù)畦贸,發(fā)生變化的,那么你就需要從舊的對(duì)象中反注冊(cè)楞捂,然后注冊(cè)新的對(duì)象薄坏。
注意:如果您希望重建與此狀態(tài)關(guān)聯(lián)的Widget,則此方法基本上是'initState'的替代寨闹!
這個(gè)方法胶坠,會(huì)自動(dòng)調(diào)用build
,所以不需要去調(diào)用setState
@override
void didUpdateWidget(Widget oldWidget) {
if (oldWidget.importantProperty != widget.importantProperty) {
_init();
}
}
7. setState()
這個(gè)方法會(huì)被framework
和開發(fā)者不斷調(diào)用。用來(lái)通知組件刷新繁堡。
這個(gè)方法的不能有異步的回調(diào)沈善。其他,就可以隨便使用椭蹄。
void updateProfile(String name) {
setState(() => this.name = name);
}
8. deactivate()
(這個(gè)狀態(tài)暫時(shí)不是很理解)
State
從樹中刪除時(shí)會(huì)調(diào)用Deactivate
闻牡,但可能會(huì)在當(dāng)前幀更改完成之前重新插入。此方法的存在主要是因?yàn)?code>State對(duì)象可以從樹中的一個(gè)點(diǎn)移動(dòng)到另一個(gè)點(diǎn)绳矩。
這很少使用罩润。
9. dispose()
State刪除對(duì)象時(shí)調(diào)用Dispose ,這是永久性的翼馆。
在此方法取消訂閱并取消所有動(dòng)畫割以,流等
10. mounted is false
state
對(duì)象被移除了,如果調(diào)用setState
应媚,會(huì)拋出的錯(cuò)誤严沥。
summary with image
一些疑問(wèn)
BuildContext
- 1. 每個(gè)widget
都有自己的context
。這個(gè)context是父組件通過(guò)build方法給他返回的中姜。
首先消玄,先看下面代碼。我們將在四個(gè)地方打印context的hashCode,來(lái)看看有什么不同
//...
_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.
);
}
}
很明顯可以看到丢胚,我們?cè)?code>initState方法時(shí)翩瓜,已經(jīng)分配拿到了父組件的BuildContext.接下來(lái)的直接使用context,也都是同一個(gè)嗜桌。
我們知道可以通過(guò)Scaffold的context來(lái)彈出一個(gè)SnackBar
奥溺。這里想通過(guò)點(diǎn)擊彈出這個(gè)。
修改代碼如下:
//...
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),
),
運(yùn)行骨宠,但是運(yùn)行報(bào)錯(cuò)信息如下:
很明顯浮定。通過(guò)上面的測(cè)試相满,我們知道這里的context,確實(shí)不是Scaffold。那我們要如何在這里拿到Scaffold的context呢桦卒?
2. 通過(guò)builder方法
修改代碼如下立美,通過(guò)Builder方法,得到這個(gè)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),
);
},
)
運(yùn)行結(jié)果
我們可以看到方灾,我們確實(shí)拿到了Scaffold
分配的Context
,而且彈出了SnackBar
.
后續(xù)過(guò)程中建蹄,一定要注意這個(gè)Context的使用。
注意:這里其實(shí)還有另外一個(gè)方法裕偿,來(lái)得到這個(gè)BuildContext
洞慎。就是將FloatingActionButton分離出來(lái),寫成另外一個(gè)組件嘿棘,就能通過(guò)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
修改成這個(gè)類
//...
floatingActionButton: ScaffoldButton(
onPressedButton: () {
_incrementCounter();
},
));
不知所云的構(gòu)造參數(shù) Key
隨意點(diǎn)開一個(gè)Widget
,就會(huì)發(fā)現(xiàn),可以傳遞一個(gè)參數(shù)Key
.那這個(gè)Key到底是干啥子鸟妙,有什么用呢焦人?
Flutter是受React啟發(fā)的,所以Virtual Dom的diff算法也參考過(guò)來(lái)了(應(yīng)該是略有修改)重父,在diff的過(guò)程中如果節(jié)點(diǎn)有Key來(lái)比較的話花椭,能夠最大程度重用已有的節(jié)點(diǎn)(特別在列表的場(chǎng)景),除了這一點(diǎn)這個(gè)Key也用在很多其他的地方這個(gè)以后會(huì)總結(jié)一下房午】罅桑總之,這里我們可以知道key能夠提高性能歪沃,所以每個(gè)Widget都會(huì)構(gòu)建方法都會(huì)有一個(gè)key的參數(shù)可選嗦锐,貫穿著整個(gè)框架。
通常情況下沪曙,我們不需要去傳遞這個(gè)Key
。因?yàn)?code>framework會(huì)在內(nèi)部自處理它萎羔,來(lái)區(qū)分不同的widgets
下面有幾種情況液走,我們可以使用它
- 使用ObjectKey
和ValueKey
來(lái)對(duì)組件進(jìn)行區(qū)分。
可以看PageStorageKey, 和另外一個(gè)例子贾陷,這個(gè)例子是deletion: https://flutter.io/cookbook/gestures/dismissible/.
簡(jiǎn)單的來(lái)說(shuō)缘眶,當(dāng)我們使用Row
或者Column
時(shí),想要執(zhí)行一個(gè)remove
的動(dòng)畫
new AnimatedList(
children: [
new Card(child: new Text("foo")),
new Card(child: new Text("bar")),
new Card(child: new Text("42")),
]
)
當(dāng)我們移除"bar"后
new AnimatedList(
children: [
new Card(child: new Text("foo")),
new Card(child: new Text("42")),
]
)
因?yàn)槲覀儧]有定義Key,所以可能flutter并不知道髓废,我們那個(gè)item發(fā)生了改變巷懈,所以可能發(fā)生在位置1上的動(dòng)畫,可能發(fā)生在其他位置慌洪。
正確的修改如下:
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")),
]
)
這樣當(dāng)我們移除"bar"的時(shí)候顶燕,flutter就能準(zhǔn)確的區(qū)別到正確的位置上凑保。
Key
雖然不是Index
,但是對(duì)于每一個(gè)元素來(lái)說(shuō),是獨(dú)一無(wú)二的涌攻。
- 使用GlobalKey
- 使用
GlobalKey
的場(chǎng)景是,從父控件和跨子Widget
來(lái)傳遞狀態(tài)時(shí)欧引。
需要注意的是:不要濫用GlobalKey,如果有更好的方式的恳谎,請(qǐng)使用其他方式來(lái)傳遞狀態(tài)芝此。
這里有一個(gè)例子是 通過(guò)給Scaffold添加GolbalKey。然后通widget.GolbalKey.state來(lái)調(diào)用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,
//...
)
}
}
這樣就可以直接從父控件調(diào)用子Widget
的狀態(tài)因痛。
推薦視頻 When to Use Keys - Flutter Widgets 101 Ep. 4
使用LocalKey婚苹,只能在同一層級(jí)上找相同的key,如果包裹了一層的話,就不行了鸵膏。
這個(gè)時(shí)候膊升,可以用globalkey來(lái)完成需求。
valuekey適合于當(dāng)個(gè)變量较性,objectKey應(yīng)用于Object用僧。GlobalKey是全局的
- 還有一個(gè)場(chǎng)景是,過(guò)渡動(dòng)畫赞咙,當(dāng)兩個(gè)頁(yè)面都是相同的Widget時(shí)责循,也可以使用GlobalKey。
總結(jié)
這邊文章攀操,我們對(duì)StateFulWidget有了升入的認(rèn)識(shí)院仿。
- 認(rèn)識(shí)了通用的控件
- 了解了StatefulWidget的生命周期
- 對(duì)BuildContext 了解。
- 對(duì)Key的場(chǎng)景進(jìn)行了了解速和。得到了使用GlobalKey來(lái)跨子組件傳遞狀態(tài)的方式歹垫。
下一遍文章:我們將更加深入的對(duì)Flutter的界面開發(fā)的一些原理
參考文章
Flutter Widgets
Flutter中的Key,LocalKey颠放,GlobalKey... And More
what-are-keys-used-for-in-flutter-framework