在前面的介紹中,我們知道在 Flutter 中幾乎所有的對象都是一個Widget。與原生開發(fā)中“控件”不同的是,F(xiàn)lutter 中的 Widget 的概念更廣泛,它不僅可以表示 UI 元素,也可以表示一些功能性的組件如:用于手勢檢測的 GestureDetector
widget、用于 APP 主題數(shù)據(jù)傳遞的Theme
等等,而原生開發(fā)中的控件通常只是指 UI 元素。在后面的內(nèi)容中,我們在描述 UI 元素時可能會用到“控件”、“組件”這樣的概念,讀者心里需要知道他們就是 widget,只是在不同場景的不同表述而已。由于 Flutter 主要就是用于構(gòu)建用戶界面的,所以,在大多數(shù)時候,讀者可以認(rèn)為 widget 就是一個控件,不必糾結(jié)于概念。
在 Flutter 中,Widget 的功能是“描述一個 UI 元素的配置數(shù)據(jù)”,它就是說,Widget 其實并不是表示最終繪制在設(shè)備屏幕上的顯示元素,而它只是描述顯示元素的一個配置數(shù)據(jù)。
實際上,F(xiàn)lutter 中真正代表屏幕上顯示元素的類是Element
,也就是說 Widget 只是描述Element
的配置數(shù)據(jù)!有關(guān)Element
的詳細(xì)介紹我們將在本書后面的高級部分深入介紹,現(xiàn)在,讀者只需要知道:Widget 只是 UI 元素的一個配置數(shù)據(jù),并且一個 Widget 可以對應(yīng)多個Element
。這是因為同一個 Widget 對象可以被添加到 U 樹的不同部分,而真正渲染時,UI樹的每一個Element
節(jié)點都會對應(yīng)一個 Widget 對象??偨Y(jié)一下:
Element
的配置數(shù)據(jù),Widget 樹實際上是一個配置樹,而真正的 UI 渲染樹是由Element
構(gòu)成;不過,由于Element
是通過 Widget 生成的,所以它們之間有對應(yīng)關(guān)系,在大多數(shù)場景,我們可以寬泛地認(rèn)為 Widget 樹就是指 UI 控件樹或 UI 渲染樹。Element
對象。這很好理解,根據(jù)同一份配置(Widget),可以創(chuàng)建多個實例(Element)。讀者應(yīng)該將這兩點牢記在心中。
我們先來看一下Widget類的聲明:
@immutable
abstract class Widget extends DiagnosticableTree {
const Widget({ this.key });
final Key key;
@protected
Element createElement();
@override
String toStringShort() {
return key == null ? '$runtimeType' : '$runtimeType-$key';
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.dense;
}
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
Widget
類繼承自DiagnosticableTree
,DiagnosticableTree
即“診斷樹”,主要作用是提供調(diào)試信息。Key
: 這個key
屬性類似于 React/Vue 中的key
,主要的作用是決定是否在下一次build
時復(fù)用舊的 widget,決定的條件在canUpdate()
方法中。createElement()
:正如前文所述“一個 Widget 可以對應(yīng)多個Element
”;Flutter Framework 在構(gòu)建 UI 樹時,會先調(diào)用此方法生成對應(yīng)節(jié)點的Element
對象。此方法是 Flutter Framework 隱式調(diào)用的,在我們開發(fā)過程中基本不會調(diào)用到。debugFillProperties(...)
復(fù)寫父類的方法,主要是設(shè)置診斷樹的一些特性。canUpdate(...)
是一個靜態(tài)方法,它主要用于在 Widget 樹重新build
時復(fù)用舊的 widget,其實具體來說,應(yīng)該是:是否用新的Widget對象去更新舊 UI 樹上所對應(yīng)的Element
對象的配置;通過其源碼我們可以看到,只要newWidget
與oldWidget
的runtimeType
和key
同時相等時就會用newWidget
去更新Element
對象的配置,否則就會創(chuàng)建新的Element
。有關(guān) Key 和 Widget 復(fù)用的細(xì)節(jié)將會在本書后面高級部分深入討論,讀者現(xiàn)在只需知道,為 Widget 顯式添加 key 的話可能(但不一定)會使UI在重新構(gòu)建時變的高效,讀者目前可以先忽略此參數(shù)。本書后面的示例中,只會在構(gòu)建列表項 UI 時會顯式指定 Key。
另外Widget
類本身是一個抽象類,其中最核心的就是定義了createElement()
接口,在 Flutter 開發(fā)中,我們一般都不用直接繼承Widget
類來實現(xiàn)一個新組件,相反,我們通常會通過繼承StatelessWidget
或StatefulWidget
來間接繼承Widget
類來實現(xiàn)。StatelessWidget
和StatefulWidget
都是直接繼承自Widget
類,而這兩個類也正是 Flutter 中非常重要的兩個抽象類,它們引入了兩種 Widget 模型,接下來我們將重點介紹一下這兩個類。
在之前的章節(jié)中,我們已經(jīng)簡單介紹過StatelessWidget
,StatelessWidget
相對比較簡單,它繼承自Widget
類,重寫了createElement()
方法:
@override
StatelessElement createElement() => new StatelessElement(this);
StatelessElement
間接繼承自Element
類,與StatelessWidget
相對應(yīng)(作為其配置數(shù)據(jù))。
StatelessWidget
用于不需要維護(hù)狀態(tài)的場景,它通常在build
方法中通過嵌套其它 Widget 來構(gòu)建UI,在構(gòu)建過程中會遞歸的構(gòu)建其嵌套的 Widget。我們看一個簡單的例子:
class Echo extends StatelessWidget {
const Echo({
Key key,
@required this.text,
this.backgroundColor:Colors.grey,
}):super(key:key);
final String text;
final Color backgroundColor;
@override
Widget build(BuildContext context) {
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}
上面的代碼,實現(xiàn)了一個回顯字符串的Echo
widget。
按照慣例,
widget
的構(gòu)造函數(shù)參數(shù)應(yīng)使用命名參數(shù),命名參數(shù)中的必要參數(shù)要添加@required
標(biāo)注,這樣有利于靜態(tài)代碼分析器進(jìn)行檢查。另外,在繼承widget
時,第一個參數(shù)通常應(yīng)該是Key
,另外,如果 Widget 需要接收子 Widget,那么child
或children
參數(shù)通常應(yīng)被放在參數(shù)列表的最后。同樣是按照慣例,Widget 的屬性應(yīng)盡可能的被聲明為final
,防止被意外改變。
然后我們可以通過如下方式使用它:
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
運(yùn)行后效果如圖3-1所示:
build
方法有一個context
參數(shù),它是BuildContext
類的一個實例,表示當(dāng)前 widget 在 widget 樹中的上下文,每一個 widget 都會對應(yīng)一個 context 對象(因為每一個 widget 都是 widget 樹上的一個節(jié)點)。實際上,context
是當(dāng)前 widget 在 widget 樹中位置中執(zhí)行”相關(guān)操作“的一個句柄,比如它提供了從當(dāng)前 widget 開始向上遍歷 widget 樹以及按照 widget 類型查找父級 widget 的方法。下面是在子樹中獲取父級 widget 的一個示例:
class ContextRoute extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Context測試"),
),
body: Container(
child: Builder(builder: (context) {
// 在Widget樹中向上查找最近的父級`Scaffold` widget
Scaffold scaffold = context.findAncestorWidgetOfExactType<Scaffold>();
// 直接返回 AppBar的title, 此處實際上是Text("Context測試")
return (scaffold.appBar as AppBar).title;
}),
),
);
}
}
運(yùn)行后效果如圖3-1-1所示:
注意:對于
BuildContext
讀者現(xiàn)在可以先作了解,隨著本書后面內(nèi)容的展開,也會用到Context的一些方法,讀者可以通過具體的場景對其有個直觀的認(rèn)識。關(guān)于BuildContext
更多的內(nèi)容,我們也將在后面高級部分再深入介紹。
和StatelessWidget
一樣,StatefulWidget
也是繼承自Widget
類,并重寫了createElement()
方法,不同的是返回的Element
對象并不相同;另外StatefulWidget
類中添加了一個新的接口createState()
。
下面我們看看StatefulWidget
的類定義:
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => new StatefulElement(this);
@protected
State createState();
}
StatefulElement
間接繼承自Element
類,與 StatefulWidget 相對應(yīng)(作為其配置數(shù)據(jù))。StatefulElement
中可能會多次調(diào)用createState()
來創(chuàng)建狀態(tài)(State)對象。createState()
用于創(chuàng)建和 Stateful widget 相關(guān)的狀態(tài),它在 Stateful widget 的生命周期中可能會被多次調(diào)用。例如,當(dāng)一個 Stateful widget 同時插入到 widget 樹的多個位置時,F(xiàn)lutter framework 就會調(diào)用該方法為每一個位置生成一個獨(dú)立的 State 實例,其實,本質(zhì)上就是一個StatefulElement
對應(yīng)一個 State 實例。在本書中經(jīng)常會出現(xiàn)“樹”的概念,在不同的場景可能指不同的意思,在說“widget 樹”時它可以指 widget 結(jié)構(gòu)樹,但由于 widget 與 Element 有對應(yīng)關(guān)系(一可能對多),在有些場景(Flutter 的 SDK 文檔中)也代指“UI 樹”的意思。而在 stateful widget 中,State 對象也和
StatefulElement
具有對應(yīng)關(guān)系(一對一),所以在 Flutter 的 SDK 文檔中,可以經(jīng)??吹健皬臉渲幸瞥?State 對象”或“插入 State 對象到樹中”這樣的描述。其實,無論哪種描述,其意思都是在描述“一棵構(gòu)成用戶界面的節(jié)點元素的樹”,讀者不必糾結(jié)于這些概念,還是那句話“得其神,忘其形”,因此,本書中出現(xiàn)的各種“樹”,如果沒有特別說明,讀者都可抽象的認(rèn)為它是“一棵構(gòu)成用戶界面的節(jié)點元素的樹”。
一個 StatefulWidget 類會對應(yīng)一個 State 類,State 表示與其對應(yīng)的 StatefulWidget 要維護(hù)的狀態(tài), State 中的保存的狀態(tài)信息可以:
setState()
方法通知 Flutter framework 狀態(tài)發(fā)生改變,F(xiàn)lutter framework 在收到消息后,會重新調(diào)用其build
方法重新構(gòu)建 widget 樹,從而達(dá)到更新 UI 的目的。State中有兩個常用屬性:
widget
,它表示與該 State 實例關(guān)聯(lián)的 widget 實例,由 Flutter framework 動態(tài)設(shè)置。注意,這種關(guān)聯(lián)并非永久的,因為在應(yīng)用生命周期中,UI 樹上的某一個節(jié)點的 widget 實例在重新構(gòu)建時可能會變化,但 State 實例只會在第一次插入到樹中時被創(chuàng)建,當(dāng)在重新構(gòu)建時,如果 widget 被修改了,F(xiàn)lutter framework 會動態(tài)設(shè)置 State.widget 為新的 widget 實例。context
。StatefulWidget 對應(yīng)的 BuildContext,作用同 StatelessWidget 的 BuildContext。理解 State 的生命周期對 flutter 開發(fā)非常重要,為了加深讀者印象,本節(jié)我們通過一個實例來演示一下 State 的生命周期。在接下來的示例中,我們實現(xiàn)一個計數(shù)器 widget,點擊它可以使計數(shù)器加1,由于要保存計數(shù)器的數(shù)值狀態(tài),所以我們應(yīng)繼承 StatefulWidget,代碼如下:
class CounterWidget extends StatefulWidget {
const CounterWidget({
Key key,
this.initValue: 0
});
final int initValue;
@override
_CounterWidgetState createState() => new _CounterWidgetState();
}
CounterWidget
接收一個initValue
整型參數(shù),它表示計數(shù)器的初始值。下面我們看一下 State 的代碼:
class _CounterWidgetState extends State<CounterWidget> {
int _counter;
@override
void initState() {
super.initState();
//初始化狀態(tài)
_counter=widget.initValue;
print("initState");
}
@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: FlatButton(
child: Text('$_counter'),
//點擊后計數(shù)器自增
onPressed:()=>setState(()=> ++_counter,
),
),
),
);
}
@override
void didUpdateWidget(CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget");
}
@override
void deactivate() {
super.deactivate();
print("deactive");
}
@override
void dispose() {
super.dispose();
print("dispose");
}
@override
void reassemble() {
super.reassemble();
print("reassemble");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("didChangeDependencies");
}
}
接下來,我們創(chuàng)建一個新路由,在新路由中,我們只顯示一個CounterWidget
:
Widget build(BuildContext context) {
return CounterWidget();
}
我們運(yùn)行應(yīng)用并打開該路由頁面,在新路由頁打開后,屏幕中央就會出現(xiàn)一個數(shù)字0,然后控制臺日志輸出:
I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build
可以看到,在 StatefulWidget 插入到 Widget 樹時首先initState
方法會被調(diào)用。
然后我們點擊??按鈕熱重載,控制臺輸出日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build
可以看到此時initState
和didChangeDependencies
都沒有被調(diào)用,而此時didUpdateWidget
被調(diào)用。
接下來,我們在 widget 樹中移除CounterWidget
,將路由build
方法改為:
Widget build(BuildContext context) {
//移除計數(shù)器
//return CounterWidget();
//隨便返回一個Text()
return Text("xxx");
}
然后熱重載,日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose
我們可以看到,在CounterWidget
從 widget 樹中移除時,deactive
和dispose
會依次被調(diào)用。
下面我們來看看各個回調(diào)函數(shù):
initState
:當(dāng) Widget 第一次插入到 Widget 樹時會被調(diào)用,對于每一個 State 對象,F(xiàn)lutter framework 只會調(diào)用一次該回調(diào),所以,通常在該回調(diào)中做一些一次性的操作,如狀態(tài)初始化、訂閱子樹的事件通知等。不能在該回調(diào)中調(diào)用BuildContext.dependOnInheritedWidgetOfExactType
(該方法用于在 Widget 樹上獲取離當(dāng)前 widget 最近的一個父級InheritFromWidget
,關(guān)于InheritedWidget
我們將在后面章節(jié)介紹),原因是在初始化完成后,Widget 樹中的InheritFromWidget
也可能會發(fā)生變化,所以正確的做法應(yīng)該在在build()
方法或didChangeDependencies()
中調(diào)用它。didChangeDependencies()
:當(dāng) State 對象的依賴發(fā)生變化時會被調(diào)用;例如:在之前build()
中包含了一個InheritedWidget
,然后在之后的build()
中InheritedWidget
發(fā)生了變化,那么此時InheritedWidget
的子 widget 的didChangeDependencies()
回調(diào)都會被調(diào)用。典型的場景是當(dāng)系統(tǒng)語言 Locale 或應(yīng)用主題改變時,F(xiàn)lutter framework 會通知 widget 調(diào)用此回調(diào)。build()
:此回調(diào)讀者現(xiàn)在應(yīng)該已經(jīng)相當(dāng)熟悉了,它主要是用于構(gòu)建 Widget 子樹的,會在如下場景被調(diào)用:
initState()
之后。didUpdateWidget()
之后。setState()
之后。didChangeDependencies()
之后。reassemble()
:此回調(diào)是專門為了開發(fā)調(diào)試而提供的,在熱重載(hot reload)時會被調(diào)用,此回調(diào)在Release模式下永遠(yuǎn)不會被調(diào)用。didUpdateWidget()
:在 widget 重新構(gòu)建時,F(xiàn)lutter framework 會調(diào)用Widget.canUpdate
來檢測 Widget 樹中同一位置的新舊節(jié)點,然后決定是否需要更新,如果Widget.canUpdate
返回true
則會調(diào)用此回調(diào)。正如之前所述,Widget.canUpdate
會在新舊widget的key和runtimeType 同時相等時會返回true,也就是說在在新舊 widget 的 key 和 runtimeType 同時相等時didUpdateWidget()
就會被調(diào)用。deactivate()
:當(dāng) State 對象從樹中被移除時,會調(diào)用此回調(diào)。在一些場景下,F(xiàn)lutter framework 會將 State 對象重新插到樹中,如包含此 State 對象的子樹在樹的一個位置移動到另一個位置時(可以通過 GlobalKey 來實現(xiàn))。如果移除后沒有重新插入到樹中則緊接著會調(diào)用dispose()
方法。dispose()
:當(dāng) State 對象從樹中被永久移除時調(diào)用;通常在此回調(diào)中釋放資源。StatefulWidget 生命周期如圖3-2所示:
注意:在繼承
StatefulWidget
重寫其方法時,對于包含@mustCallSuper
標(biāo)注的父類方法,都要在子類方法中先調(diào)用父類方法。
現(xiàn)在,我們回答之前提出的問題,為什么build()
方法放在 State(而不是StatefulWidget
)中 ?這主要是為了提高開發(fā)的靈活性。如果將build()
方法在StatefulWidget
中則會有兩個問題:
試想一下,如果我們的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)該暴露給外部。
綜上所述,可以發(fā)現(xiàn),對于StatefulWidget
,將build
方法放在State中,可以給開發(fā)帶來很大的靈活性。
由于 StatefulWidget 的的具體邏輯都在其 State 中,所以很多時候,我們需要獲取 StatefulWidget 對應(yīng)的 State 對象來調(diào)用一些方法,比如Scaffold
組件對應(yīng)的狀態(tài)類ScaffoldState
中就定義了打開 SnackBar(路由頁底部提示條)的方法。我們有兩種方法在子 widget 樹中獲取父級 StatefulWidget 的 State 對象。
context
對象有一個findAncestorStateOfType()
方法,該方法可以從當(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.findAncestorStateOfType<ScaffoldState>();
//調(diào)用ScaffoldState的showSnackBar來彈出SnackBar
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
},
child: Text("顯示SnackBar"),
);
}),
),
);
上面示例運(yùn)行后,點擊”顯示 SnackBar“,效果如圖3-1-2所示:
一般來說,如果 StatefulWidget 的狀態(tài)是私有的(不應(yīng)該向外部暴露),那么我們代碼中就不應(yīng)該去直接獲取其 State 對象;如果 StatefulWidget 的狀態(tài)是希望暴露出的(通常還有一些組件的操作方法),我們則可以去直接獲取其 State 對象。但是通過context.findAncestorStateOfType
獲取 StatefulWidget 的狀態(tài)的方法是通用的,我們并不能在語法層面指定 StatefulWidget 的狀態(tài)是否私有,所以在 Flutter 開發(fā)中便有了一個默認(rèn)的約定:如果 StatefulWidget 的狀態(tài)是希望暴露出的,應(yīng)當(dāng)在 StatefulWidget 中提供一個of
靜態(tài)方法來獲取其 State 對象,開發(fā)者便可直接通過該方法來獲??;如果 State 不希望暴露,則不提供of
方法。這個約定在 Flutter SDK 里隨處可見。所以,上面示例中的Scaffold
也提供了一個of
方法,我們其實是可以直接調(diào)用它的:
...//省略無關(guān)代碼
// 直接通過of靜態(tài)方法來獲取ScaffoldState
ScaffoldState _state=Scaffold.of(context);
_state.showSnackBar(
SnackBar(
content: Text("我是SnackBar"),
),
);
Flutter 還有一種通用的獲取State
對象的方法——通過 GlobalKey 來獲??! 步驟分兩步:
StatefulWidget
添加GlobalKey
。 //定義一個globalKey, 由于GlobalKey要保持全局唯一性,我們使用靜態(tài)變量存儲
static GlobalKey<ScaffoldState> _globalKey= GlobalKey();
...
Scaffold(
key: _globalKey , //設(shè)置key
...
)
GlobalKey
來獲取State
對象 _globalKey.currentState.openDrawer()
GlobalKe y是 Flutter 提供的一種在整個 APP 中引用 element 的機(jī)制。如果一個 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 提供了一套豐富、強(qiáng)大的基礎(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';
下面我們介紹一下常用的組件。
Text
(opens new window):該組件可讓您創(chuàng)建一個帶格式的文本。Row
(opens new window)、 Column
(opens new window): 這些具有彈性空間的布局類 Widget 可讓您在水平(Row)和垂直(Column)方向上創(chuàng)建靈活的布局。其設(shè)計是基于Web開發(fā)中的 Flexbox 布局模型。Stack
(opens new window): 取代線性布局 (譯者語:和 Android 中的FrameLayout
相似),Stack
(opens new window)允許子 widget 堆疊, 你可以使用 Positioned
(opens new window)來定位他們相對于Stack
的上下左右四條邊的位置。Stacks 是基于 Web 開發(fā)中的絕對定位(absolute positioning )布局模型設(shè)計的。Container
(opens new window): Container
(opens new window)可讓您創(chuàng)建矩形視覺元素。container 可以裝飾一個BoxDecoration
(opens new window), 如 background、一個邊框、或者一個陰影。 Container
(opens new window)也可以具有邊距(margins)、填充(padding)和應(yīng)用于其大小的約束(constraints)。另外, Container
(opens new window)可以使用矩陣在三維空間中對其進(jìn)行變換。
Flutter 提供了一套豐富的 Material 組件,它可以幫助我們構(gòu)建遵循 Material Design 設(shè)計規(guī)范的應(yīng)用程序。Material 應(yīng)用程序以MaterialApp
(opens new window) 組件開始, 該組件在應(yīng)用程序的根部創(chuàng)建了一些必要的組件,比如Theme
組件,它用于配置應(yīng)用的主題。 是否使用MaterialApp
(opens new window)完全是可選的,但是使用它是一個很好的做法。在之前的示例中,我們已經(jīng)使用過多個 Material 組件了,如:Scaffold
、AppBar
、FlatButton
等。要使用 Material 組件,需要先引入它:
import 'package:flutter/material.dart';
Flutter 也提供了一套豐富的 Cupertino 風(fēng)格的組件,盡管目前還沒有 Material 組件那么豐富,但是它仍在不斷的完善中。值得一提的是在 Material 組件庫中有一些組件可以根據(jù)實際運(yùn)行平臺來切換表現(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: () {}
),
),
);
}
}
下面(圖3-3)是在 iPhoneX 上頁面效果截圖:
本章后面章節(jié)的示例中會使用一些布局類組件,如Scaffold
、Row
、Column
等,這些組件將在后面“布局類組件”一章中詳細(xì)介紹,讀者可以先不用關(guān)注。
Flutter 提供了豐富的組件,在實際的開發(fā)中你可以根據(jù)需要隨意使用它們,而不必?fù)?dān)心引入過多組件庫會讓你的應(yīng)用安裝包變大,這不是 web 開發(fā),dart 在編譯時只會編譯你使用了的代碼。由于 Material 和 Cupertino 都是在基礎(chǔ)組件庫之上的,所以如果我們的應(yīng)用中引入了這兩者之一,則不需要再引入flutter/widgets.dart
了,因為它們內(nèi)部已經(jīng)引入過了。
更多建議: