為什么要將build方法放在State中,而不是放在StatefulWidget中?
這主要是為了提高開發(fā)的靈活性。如果將build()方法在StatefulWidget中則會有兩個問題:
-
狀態(tài)訪問不便。
試想一下,如果我們的
StatefulWidget有很多狀態(tài),而每次狀態(tài)改變都要調(diào)用build方法,由于狀態(tài)是保存在State中的,如果build方法在StatefulWidget中,那么build方法和狀態(tài)分別在兩個類中,那么構(gòu)建時讀取狀態(tài)將會很不方便!試想一下,如果真的將build方法放在StatefulWidget中的話,由于構(gòu)建用戶界面過程需要依賴State,所以build方法將必須加一個State參數(shù),大概是下面這樣:Widget build(BuildContext context, State state){ //state.counter ... }這樣的話就只能將State的所有狀態(tài)聲明為公開的狀態(tài),這樣才能在State類外部訪問狀態(tài)!但是,將狀態(tài)設(shè)置為公開后,狀態(tài)將不再具有私密性,這就會導(dǎo)致對狀態(tài)的修改將會變的不可控。但如果將
build()方法放在State中的話,構(gòu)建過程不僅可以直接訪問狀態(tài),而且也無需公開私有狀態(tài),這會非常方便。 -
繼承
StatefulWidget不便。例如,F(xiàn)lutter中有一個動畫widget的基類
AnimatedWidget,它繼承自StatefulWidget類。AnimatedWidget中引入了一個抽象方法build(BuildContext context),繼承自AnimatedWidget的動畫widget都要實現(xiàn)這個build方法。現(xiàn)在設(shè)想一下,如果StatefulWidget類中已經(jīng)有了一個build方法,正如上面所述,此時build方法需要接收一個state對象,這就意味著AnimatedWidget必須將自己的State對象(記為_animatedWidgetState)提供給其子類,因為子類需要在其build方法中調(diào)用父類的build方法,代碼可能如下:class MyAnimationWidget extends AnimatedWidget{ @override Widget build(BuildContext context, State state){ //由于子類要用到AnimatedWidget的狀態(tài)對象_animatedWidgetState, //所以AnimatedWidget必須通過某種方式將其狀態(tài)對象_animatedWidgetState //暴露給其子類 super.build(context, _animatedWidgetState) } }
這樣很顯然是不合理的,因為
-
AnimatedWidget的狀態(tài)對象是AnimatedWidget內(nèi)部實現(xiàn)細(xì)節(jié),不應(yīng)該暴露給外部。 - 如果要將父類狀態(tài)暴露給子類,那么必須得有一種傳遞機制,而做這一套傳遞機制是無意義的,因為父子類之間狀態(tài)的傳遞和子類本身邏輯是無關(guān)的。
綜上所述,可以發(fā)現(xiàn),對于StatefulWidget,將build方法放在State中,可以給開發(fā)帶來很大的靈活性。
在Widget樹中獲取State對象
由于StatefulWidget的的具體邏輯都在其State中,所以很多時候,我們需要獲取StatefulWidget對應(yīng)的State對象來調(diào)用一些方法,比如Scaffold組件對應(yīng)的狀態(tài)類ScaffoldState中就定義了打開SnackBar(路由頁底部提示條)的方法。我們有兩種方法在子widget樹中獲取父級StatefulWidget的State對象。
通過Context獲取
context對象有一個ancestorStateOfType(TypeMatcher)方法,該方法可以從當(dāng)前節(jié)點沿著widget樹向上查找指定類型的StatefulWidget對應(yīng)的State對象。下面是實現(xiàn)打開SnackBar的示例:
Scaffold(
appBar: AppBar(
title: Text("子樹中獲取State對象"),
),
body: Center(
child: Builder(builder: (context) {
return RaisedButton(
onPressed: () {
// 查找父級最近的Scaffold對應(yīng)的ScaffoldState對象
ScaffoldState _state = context.ancestorStateOfType(
TypeMatcher<ScaffoldState>());
//調(diào)用ScaffoldState的showSnackBar來彈出SnackBar
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
},
child: Text("顯示SnackBar"),
);
}),
),
);
上面示例運行后,點擊”顯示SnackBar“,效果如圖所示:

一般來說,如果StatefulWidget的狀態(tài)是私有的(不應(yīng)該向外部暴露),那么我們代碼中就不應(yīng)該去直接獲取其State對象;如果StatefulWidget的狀態(tài)是希望暴露出的(通常還有一些組件的操作方法),我們則可以去直接獲取其State對象。但是通過context.ancestorStateOfType獲取StatefulWidget的狀態(tài)的方法是通用的,我們并不能在語法層面指定StatefulWidget的狀態(tài)是否私有,所以在Flutter開發(fā)中便有了一個默認(rèn)的約定:如果StatefulWidget的狀態(tài)是希望暴露出的,應(yīng)當(dāng)在StatefulWidget中提供一個of靜態(tài)方法來獲取其State對象,開發(fā)者便可直接通過該方法來獲?。蝗绻鸖tate不希望暴露,則不提供of方法。這個約定在Flutter SDK里隨處可見。所以,上面示例中的Scaffold也提供了一個of方法,我們其實是可以直接調(diào)用它的:
...//省略無關(guān)代碼
// 直接通過of靜態(tài)方法來獲取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
通過GlobalKey
Flutter還有一種通用的獲取State對象的方法——通過GlobalKey來獲??! 步驟分兩步:
-
給目標(biāo)
StatefulWidget添加GlobalKey。//定義一個globalKey, 由于GlobalKey要保持全局唯一性,我們使用靜態(tài)變量存儲 static GlobalKey<ScaffoldState> _globalKey= GlobalKey(); ... Scaffold( key: _globalKey , //設(shè)置key ... ) -
通過
GlobalKey來獲取State對象_globalKey.currentState.openDrawer()
GlobalKey是Flutter提供的一種在整個APP中引用element的機制。如果一個widget設(shè)置了GlobalKey,那么我們便可以通過globalKey.currentWidget獲得該widget對象、globalKey.currentElement來獲得widget對應(yīng)的element對象,如果當(dāng)前widget是StatefulWidget,則可以通過globalKey.currentState來獲得該widget對應(yīng)的state對象。
注意:使用GlobalKey開銷較大,如果有其他可選方案,應(yīng)盡量避免使用它。另外同一個GlobalKey在整個widget樹中必須是唯一的,不能重復(fù)。
Flutter SDK內(nèi)置組件庫介紹
Flutter提供了一套豐富、強大的基礎(chǔ)組件,在基礎(chǔ)組件庫之上Flutter又提供了一套Material風(fēng)格(Android默認(rèn)的視覺風(fēng)格)和一套Cupertino風(fēng)格(iOS視覺風(fēng)格)的組件庫。要使用基礎(chǔ)組件庫,需要先導(dǎo)入:
import 'package:flutter/widgets.dart';
下面我們介紹一下常用的組件。
基礎(chǔ)組件
-
Text:該組件可讓您創(chuàng)建一個帶格式的文本。 -
Row、Column: 這些具有彈性空間的布局類Widget可讓您在水平(Row)和垂直(Column)方向上創(chuàng)建靈活的布局。其設(shè)計是基于Web開發(fā)中的Flexbox布局模型。 -
Stack: 取代線性布局 (譯者語:和Android中的FrameLayout相似),Stack允許子 widget 堆疊, 你可以使用Positioned來定位他們相對于Stack的上下左右四條邊的位置。Stacks是基于Web開發(fā)中的絕對定位(absolute positioning )布局模型設(shè)計的。 -
Container:Container可讓您創(chuàng)建矩形視覺元素。container 可以裝飾一個BoxDecoration, 如 background、一個邊框、或者一個陰影。Container也可以具有邊距(margins)、填充(padding)和應(yīng)用于其大小的約束(constraints)。另外,Container可以使用矩陣在三維空間中對其進行變換。
Material組件
Flutter提供了一套豐富的Material組件,它可以幫助我們構(gòu)建遵循Material Design設(shè)計規(guī)范的應(yīng)用程序。Material應(yīng)用程序以MaterialApp 組件開始, 該組件在應(yīng)用程序的根部創(chuàng)建了一些必要的組件,比如Theme組件,它用于配置應(yīng)用的主題。 是否使用MaterialApp完全是可選的,但是使用它是一個很好的做法。在之前的示例中,我們已經(jīng)使用過多個Material 組件了,如:Scaffold、AppBar、FlatButton等。要使用Material 組件,需要先引入它:
import 'package:flutter/material.dart';
Cupertino組件
Flutter也提供了一套豐富的Cupertino風(fēng)格的組件,盡管目前還沒有Material 組件那么豐富,但是它仍在不斷的完善中。值得一提的是在Material 組件庫中有一些組件可以根據(jù)實際運行平臺來切換表現(xiàn)風(fēng)格,比如MaterialPageRoute,在路由切換時,如果是Android系統(tǒng),它將會使用Android系統(tǒng)默認(rèn)的頁面切換動畫(從底向上);如果是iOS系統(tǒng),它會使用iOS系統(tǒng)默認(rèn)的頁面切換動畫(從右向左)。由于在前面的示例中還沒有Cupertino組件的示例,下面我們實現(xiàn)一個簡單的Cupertino組件風(fēng)格的頁面:
//導(dǎo)入cupertino widget庫
import 'package:flutter/cupertino.dart';
class CupertinoTestRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: CupertinoNavigationBar(
middle: Text("Cupertino Demo"),
),
child: Center(
child: CupertinoButton(
color: CupertinoColors.activeBlue,
child: Text("Press"),
onPressed: () {}
),
),
);
}
}
下面是在iPhoneX上頁面效果截圖:

總結(jié)
Flutter提供了豐富的組件,在實際的開發(fā)中你可以根據(jù)需要隨意使用它們,而不必?fù)?dān)心引入過多組件庫會讓你的應(yīng)用安裝包變大,這不是web開發(fā),dart在編譯時只會編譯你使用了的代碼。由于Material和Cupertino都是在基礎(chǔ)組件庫之上的,所以如果我們的應(yīng)用中引入了這兩者之一,則不需要再引入flutter/widgets.dart了,因為它們內(nèi)部已經(jīng)引入過了。