## Widget簡介
### 概念
在前面的介紹中,我們知道Flutter中幾乎所有的對象都是一個Widget,與原生開發中“控件”不同的是,Flutter中的widget的概念更廣泛,它不僅可以表示UI元素,也可以表示一些功能性的組件如:用于手勢檢測的 `GestureDetector` widget、用于應用主題數據傳遞的`Theme`等等。而原生開發中的控件通常只是指UI元素。在后面的內容中,我們在描述UI元素時,我們可能會用到“控件”、“組件”這樣的概念,讀者心里需要知道他們就是widget,只是在不同場景的不同表述而已。由于Flutter主要就是用于構建用戶界面的,所以,在大多數時候,讀者可以認為widget就是一個控件,不必糾結于概念。
### Widget與Element
在Flutter中,Widget的功能是“描述一個UI元素的配置數據”,它就是說,Widget其實并不是表示最終繪制在設備屏幕上的顯示元素,而只是顯示元素的一個配置數據。實際上,Flutter中真正代表屏幕上顯示元素的類是`Element`,也就是說Widget只是描述`Element`的一個配置,有關`Element`的詳細介紹我們將在本書后面的高級部分深入介紹,讀者現在只需要知道,Widget只是UI元素的一個配置數據,并且一個Widget可以對應多個`Element`,這是因為同一個Widget對象可以被添加到UI樹的不同部分,而真正渲染時,UI樹的每一個Widget節點都會對應一個`Element`對象。總結一下:
- Widget實際上就是Element的配置數據,Widget樹實際上是一個配置樹,而真正的UI渲染樹是由Element構成;不過,由于Element是通過Widget生成,所以它們之間有對應關系,所以在大多數場景,我們可以寬泛地認為Widget樹就是指UI控件樹或UI渲染樹。
- 一個Widget對象可以對應多個Element對象。這很好理解,根據同一份配置(Widget),可以創建多個實例(Element)。
讀者應該將這兩點牢記在心中。
### 主要接口
我們先來看一下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`即“診斷樹”,主要作用是提供調試信息。
- `Key`: 這個`key`屬性類似于React/Vue中的`key`,主要的作用是決定是否在下一次`build`時復用舊的widget,決定的條件在`canUpdate()`方法中。
- `createElement()`:正如前文所述“一個Widget可以對應多個`Element`”;Flutter Framework在構建UI樹時,會先調用此方法生成對應節點的`Element`對象。此方法是Flutter Framework隱式調用的,在我們開發過程中基本不會調用到。
- `debugFillProperties(...)` 復寫父類的方法,主要是設置診斷樹的一些特性。
- `canUpdate(...)`是一個靜態方法,它主要用于在Widget樹重新`build`時復用舊的widget,其實具體來說,應該是:是否用新的Widget對象去更新舊UI樹上所對應的`Element`對象的配置;通過其源碼我們可以看到,只要`newWidget`與`oldWidget`的`runtimeType`和`key`同時相等時就會用`newWidget`去更新`Element`對象的配置,否則就會創建新的`Element`。
有關Key和Widget復用的細節將會在本書后面高級部分深入討論,讀者現在只需知道,為Widget顯式添加key的話可能(但不一定)會使UI在重新構建時變的高效,讀者目前可以先忽略此參數。本書后面的示例中,我們只在構建列表項UI時會顯式指定Key。
另外`Widget`類本身是一個抽象類,其中最核心的就是定義了`createElement()`接口,在Flutter開發中,我們一般都不用直接繼承`Widget`類來實現Widget,相反,我們通常會通過繼承`StatelessWidget`和`StatefulWidget`來間接繼承`Widget`類來實現,而`StatelessWidget`和`StatefulWidget`都是直接繼承自`Widget`類,而這兩個類也正是Flutter中非常重要的兩個抽象類,它們引入了兩種Widget模型,接下來我們將重點介紹一下這兩個類。
## Stateless Widget
在之前的章節中,我們已經簡單介紹過StatelessWidget,StatelessWidget相對比較簡單,它繼承自`Widget`,重寫了`createElement()`方法:
```
@override
StatelessElement createElement() => new StatelessElement(this);
```
`StatelessElement` 間接繼承自`Element`類,與StatelessWidget相對應(作為其配置數據)。
StatelessWidget用于不需要維護狀態的場景,它通常在`build`方法中通過嵌套其它Widget來構建UI,在構建過程中會遞歸的構建其嵌套的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),
),
);
}
}
```
上面的代碼,實現了一個回顯字符串的`Echo` widget。
> 按照慣例,widget的構造函數應使用命名參數,命名參數中的必要參數要添加@required標注,這樣有利于靜態代碼分析器進行檢查,另外,在繼承widget時,第一個參數通常應該是`Key`,如果接受子widget的child參數,那么通常應該將它放在參數列表的最后。同樣是按照慣例,widget的屬性應被聲明為`final`,防止被意外改變。
然后我們可以通過如下方式使用它:
```
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
```

## Stateful Widget
和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相對應(作為其配置數據)。`StatefulElement`中可能會多次調用`createState()`來創建狀態(State)對象。
- `createState()` 用于創建和Stateful widget相關的狀態,它在Stateful widget的生命周期中可能會被多次調用。例如,當一個Stateful widget同時插入到widget樹的多個位置時,Flutter framework就會調用該方法為每一個位置生成一個獨立的State實例,其實,本質上就是一個`StatefulElement`對應一個State實例。
> 在本書中經常會出現“樹“的概念,在不同的場景可能指不同的意思,在說“widget樹”時它可以指widget結構樹,但由于widget與Element有對應關系(一可能對多),在有些場景(Flutter的SDK文檔中)也代指“UI樹”的意思。而在stateful widget中,State對象也和`StatefulElement`具有對應關系(一對一),所以在Flutter的SDK文檔中,可以經常看到“從樹中移除State對象”或“插入State對象到樹中”這樣的描述。其實,無論哪種描述,其意思都是在描述“一棵構成用戶界面的節點元素的樹”,讀者不必糾結于這些概念,還是那句話“得其神,忘其形”,因此,本書中出現的各種“樹”,如果沒有特別說明,讀者都可抽象的認為它是“一棵構成用戶界面的節點元素的樹”。
### State
一個StatefulWidget類會對應一個State類,State表示與其對應的StatefulWidget要維護的狀態,State中的保存的狀態信息可以:
1. 在widget build時可以被同步讀取。
2. 在widget生命周期中可以被改變,當State被改變時,可以手動調用其`setState()`方法通知Flutter framework狀態發生改變,Flutter framework在收到消息后,會重新調用其`build`方法重新構建widget樹,從而達到更新UI的目的。
State中有兩個常用屬性:
1. `widget`,它表示與該State實例關聯的widget實例,由Flutter framework動態設置。注意,這種關聯并非永久的,因為在應用聲明周期中,UI樹上的某一個節點的widget實例在重新構建時可能會變化,但State實例只會在第一次插入到樹中時被創建,當在重新構建時,如果widget被修改了,Flutter framework會動態設置State.widget為新的widget實例。
2. `context`,它是`BuildContext`類的一個實例,表示構建widget的上下文,它是操作widget在樹中位置的一個句柄,它包含了一些查找、遍歷當前Widget樹的一些方法。每一個widget都有一個自己的context對象。
> 對于`BuildContext`讀者現在可以先作了解,隨著本書后面內容的展開,也會用到Context的一些方法,讀者可以通過具體的場景對其有個直觀的認識。關于`BuildContext`更多的內容,我們也將在后面高級部分再深入介紹。
#### State生命周期
理解State的生命周期對flutter開發非常重要,為了加深讀者印象,本節我們通過一個實例來演示一下State的生命周期。在接下來的示例中,我們實現一個計數器widget,點擊它可以使計數器加1,由于要保存計數器的數值狀態,所以我們應繼承StatefulWidget,代碼如下:
```
class CounterWidget extends StatefulWidget {
const CounterWidget({
Key key,
this.initValue: 0
});
final int initValue;
@override
_CounterWidgetState createState() => new _CounterWidgetState();
}
```
`CounterWidget`接收一個`initValue`整形參數,它表示計數器的初始值。下面我們看一下State的代碼:
```
class _CounterWidgetState extends State<CounterWidget> {
int _counter;
@override
void initState() {
super.initState();
//初始化狀態
_counter=widget.initValue;
print("initState");
}
@override
Widget build(BuildContext context) {
print("build");
return Center(
child: FlatButton(
child: Text('$_counter'),
//點擊后計數器自增
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");
}
}
```
接下來,我們創建一個新路由,在新路由中,我們只顯示一個`CounterWidget`:
```
Widget build(BuildContext context) {
return CounterWidget();
}
```
我們運行應用并打開該路由頁面,在新路由頁打開后,屏幕中央就會出現一個數字0,然后控制臺日志輸出:
```
I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build
```
可以看到,在StatefulWidget插入到Widget樹時首先`initState`方法會被調用。
然后我們點擊??按鈕熱重載,控制臺輸出日志如下:
```
I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build
```
可以看到此時`initState` 和`didChangeDependencies`都沒有被調用,而此時`didUpdateWidget`被調用。
接下來,我們在widget樹中移除`CounterWidget`,將路由`build`方法改為:
```
Widget build(BuildContext context) {
//移除計數器
//return CounterWidget();
//隨便返回一個Text()
return Text("xxx");
}
```
然后熱重載,日志如下:
```
I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose
```
我們可以看到,在`CounterWidget`從widget樹中移除時,`deactive`和`dispose`會依次被調用。
下面我們來看看各個回調函數:
- `initState`:當Widget第一次插入到Widget樹時會被調用,對于每一個State對象,Flutter framework只會調用一次該回調,所以,通常在該回調中做一些一次性的操作,如狀態初始化、訂閱子樹的事件通知等。不能在該回調中調用`BuildContext.inheritFromWidgetOfExactType`(該方法用于在Widget樹上獲取離當前widget最近的一個父級`InheritFromWidget`,關于`InheritedWidget`我們將在后面章節介紹),原因是在初始化完成后,Widget樹中的`InheritFromWidget`也可能會發生變化,所以正確的做法應該在在`build()`方法或`didChangeDependencies()`中調用它。
- `didChangeDependencies()`:當State對象的依賴發生變化時會被調用;例如:在之前`build()` 中包含了一個`InheritedWidget`,然后在之后的`build()` 中`InheritedWidget`發生了變化,那么此時`InheritedWidget`的子widget的`didChangeDependencies()`回調都會被調用。典型的場景是當系統語言Locale或應用主題改變時,Flutter framework會通知widget調用此回調。
- `build()`:此回調讀者現在應該已經相當熟悉了,它主要是用于構建Widget子樹的,會在如下場景被調用:
1. 在調用`initState()`之后。
2. 在調用`didUpdateWidget()`之后。
3. 在調用`setState()`之后。
4. 在調用`didChangeDependencies()`之后。
5. 在State對象從樹中一個位置移除后(會調用deactivate)又重新插入到樹的其它位置之后。
- `reassemble()`:此回調是專門為了開發調試而提供的,在熱重載(hot reload)時會被調用,此回調在Release模式下永遠不會被調用。
- `didUpdateWidget()`:在widget重新構建時,Flutter framework會調用`Widget.canUpdate`來檢測Widget樹中同一位置的新舊節點,然后決定是否需要更新,如果`Widget.canUpdate`返回`true`則會調用此回調。正如之前所述,`Widget.canUpdate`會在新舊widget的key和runtimeType同時相等時會返回true,也就是說在在新舊widget的key和runtimeType同時相等時`didUpdateWidget()`就會被調用。
- `deactivate()`:當State對象從樹中被移除時,會調用此回調。在一些場景下,Flutter framework會將State對象重新插到樹中,如包含此State對象的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey來實現)。如果移除后沒有重新插入到樹中則緊接著會調用`dispose()`方法。
- `dispose()`:當State對象從樹中被永久移除時調用;通常在此回調中釋放資源。
> todo: 這里缺一張生命周期圖
注意:在繼承StatefulWidget重寫其方法時,對于包含@mustCallSuper標注的父類方法,都要在子類方法中先調用父類方法。
## 狀態管理
響應式的編程框架中都會有一個永恒的主題——“狀態管理”,無論是在React/Vue(兩者都是支持響應式編程的web開發框架)還是Flutter,他們討論的問題和解決的思想都是一致的。所以,如果你對React/Vue的狀態管理有了解,可以跳過本節。言歸正傳,我們想一個問題,stateful widget的狀態應該被誰管理?widget本身?父widget?都會?還是另一個對象?答案是取決于實際情況!以下是管理狀態的最常見的方法:
- Widget管理自己的state。
- 父widget管理子widget狀態。
- 混合管理(父widget和子widget都管理狀態)。
如何決定使用哪種管理方法?以下原則可以幫助你決定:
- 如果狀態是用戶數據,如復選框的選中狀態、滑塊的位置,則該狀態最好由父widget管理。
- 如果狀態是有關界面外觀效果的,例如顏色、動畫,那么狀態最好由widget本身來管理。
- 如果某一個狀態是不同widget共享的則最好由它們共同的父widget管理。
在widget內部管理狀態封裝性會好一些,而在父widget中管理會比較靈活。有些時候,如果不確定到底該怎么管理狀態,那么推薦的首選是在父widget中管理(靈活會顯得更重要一些)。
接下來,我們將通過創建三個簡單示例TapboxA、TapboxB和TapboxC來說明管理狀態的不同方式。 這些例子功能是相似的 ——創建一個盒子,當點擊它時,盒子背景會在綠色與灰色之間切換。狀態 `_active`確定顏色:綠色為`true` ,灰色為`false`。

下面的例子將使用GestureDetector來識別點擊事件,關于該GestureDetector的詳細內容我們將在后面“事件處理”一章中介紹。
### Widget管理自身狀態
\_TapboxAState 類:
- 管理TapboxA的狀態。
- 定義`_active`:確定盒子的當前顏色的布爾值。
- 定義`_handleTap()`函數,該函數在點擊該盒子時更新`_active`,并調用`setState()`更新UI。
- 實現widget的所有交互式行為。
```
// TapboxA 管理自身狀態.
//------------------------- TapboxA ----------------------------------
class TapboxA extends StatefulWidget {
TapboxA({Key key}) : super(key: key);
@override
_TapboxAState createState() => new _TapboxAState();
}
class _TapboxAState extends State<TapboxA> {
bool _active = false;
void _handleTap() {
setState(() {
_active = !_active;
});
}
Widget build(BuildContext context) {
return new GestureDetector(
onTap: _handleTap,
child: new Container(
child: new Center(
child: new Text(
_active ? 'Active' : 'Inactive',
style: new TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: new BoxDecoration(
color: _active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
```
### 父widget管理子widget的state
對于父widget來說,管理狀態并告訴其子widget何時更新通常是比較好的方式。 例如,IconButton是一個圖片按鈕,但它是一個無狀態的widget,因為我們認為父widget需要知道該按鈕是否被點擊來采取相應的處理。
在以下示例中,TapboxB通過回調將其狀態導出到其父項。由于TapboxB不管理任何狀態,因此它的父類為StatelessWidget。
ParentWidgetState 類:
- 為TapboxB 管理`_active`狀態.
- 實現`_handleTapboxChanged()`,當盒子被點擊時調用的方法.
- 當狀態改變時,調用`setState()`更新UI.
TapboxB 類:
- 繼承`StatelessWidget`類,因為所有狀態都由其父widget處理。
- 當檢測到點擊時,它會通知父widget。
```
// ParentWidget 為 TapboxB 管理狀態.
//------------------------ ParentWidget --------------------------------
class ParentWidget extends StatefulWidget {
@override
_ParentWidgetState createState() => new _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return new Container(
child: new TapboxB(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//------------------------- TapboxB ----------------------------------
class TapboxB extends StatelessWidget {
TapboxB({Key key, this.active: false, @required this.onChanged})
: super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
void _handleTap() {
onChanged(!active);
}
Widget build(BuildContext context) {
return new GestureDetector(
onTap: _handleTap,
child: new Container(
child: new Center(
child: new Text(
active ? 'Active' : 'Inactive',
style: new TextStyle(fontSize: 32.0, color: Colors.white),
),
),
width: 200.0,
height: 200.0,
decoration: new BoxDecoration(
color: active ? Colors.lightGreen[700] : Colors.grey[600],
),
),
);
}
}
```
### 混合管理
對于一些widget來說,混合管理的方式非常有用。在這種情況下,widget自身管理一些內部狀態,而父widget管理一些其他外部狀態。
在下面TapboxC示例中,按下時,盒子的周圍會出現一個深綠色的邊框。抬起時,邊框消失;點擊生效,盒子的顏色改變。 TapboxC將其`_active`狀態導出到其父widget中,但在內部管理其`_highlight`狀態。這個例子有兩個狀態對象`_ParentWidgetState`和`_TapboxCState`。
\_ParentWidgetStateC 對象:
- 管理`_active` 狀態。
- 實現 `_handleTapboxChanged()` ,當盒子被點擊時調用。
- 當點擊盒子并且`_active`狀態改變時調用`setState()`更新UI。
\_TapboxCState 對象:
- 管理`_highlight` state。
- `GestureDetector`監聽所有tap事件。當用戶點下時,它添加高亮(深綠色邊框);當用戶釋放時,會移除高亮。
- 當按下、抬起、或者取消點擊時更新`_highlight`狀態,調用`setState()`更新UI。
- 當點擊時,將狀態的改變傳遞給父widget.
```
//---------------------------- ParentWidget ----------------------------
class ParentWidgetC extends StatefulWidget {
@override
_ParentWidgetCState createState() => new _ParentWidgetCState();
}
class _ParentWidgetCState extends State<ParentWidgetC> {
bool _active = false;
void _handleTapboxChanged(bool newValue) {
setState(() {
_active = newValue;
});
}
@override
Widget build(BuildContext context) {
return new Container(
child: new TapboxC(
active: _active,
onChanged: _handleTapboxChanged,
),
);
}
}
//----------------------------- TapboxC ------------------------------
class TapboxC extends StatefulWidget {
TapboxC({Key key, this.active: false, @required this.onChanged})
: super(key: key);
final bool active;
final ValueChanged<bool> onChanged;
_TapboxCState createState() => new _TapboxCState();
}
class _TapboxCState extends State<TapboxC> {
bool _highlight = false;
void _handleTapDown(TapDownDetails details) {
setState(() {
_highlight = true;
});
}
void _handleTapUp(TapUpDetails details) {
setState(() {
_highlight = false;
});
}
void _handleTapCancel() {
setState(() {
_highlight = false;
});
}
void _handleTap() {
widget.onChanged(!widget.active);
}
Widget build(BuildContext context) {
// 在按下時添加綠色邊框,當抬起時,取消高亮
return new GestureDetector(
onTapDown: _handleTapDown, // 處理按下事件
onTapUp: _handleTapUp, // 處理抬起事件
onTap: _handleTap,
onTapCancel: _handleTapCancel,
child: new Container(
child: new Center(
child: new Text(widget.active ? 'Active' : 'Inactive',
style: new TextStyle(fontSize: 32.0, color: Colors.white)),
),
width: 200.0,
height: 200.0,
decoration: new BoxDecoration(
color: widget.active ? Colors.lightGreen[700] : Colors.grey[600],
border: _highlight
? new Border.all(
color: Colors.teal[700],
width: 10.0,
)
: null,
),
),
);
}
}
```
另一種實現可能會將高亮狀態導出到父widget,同時保持`_active`狀態為內部,但如果你要將該TapBox給其它人使用,可能沒有什么意義。 開發人員只會關心該框是否處于Active狀態,而不在乎高亮顯示是如何管理的,所以應該讓TapBox內部處理這些細節。
### 全局狀態管理
當應用中包括一些跨widget(甚至跨路由)的狀態需要同步時,上面介紹的方法很難勝任了。比如,我們有一個設置頁,里面可以設置應用語言,但是我們為了讓設置實時生效,我們期望在語言狀態發生改變時,我們的APP Widget能夠重新build一下,但我們的APP Widget和設置頁并不在一起。正確的做法是通過一個全局狀態管理器來處理這種“相距較遠”的widget之間的通信。目前主要有兩種辦法:
1. 實現一個全局的事件總線,將語言狀態改變對應為一個事件,然后在APP Widget所在的父widget`initState` 方法中訂閱語言改變的事件,當用戶在設置頁切換語言后,我們觸發語言改變事件,然后APP Widget那邊就會收到通知,然后重新`build`一下即可。
2. 使用redux這樣的全局狀態包,讀者可以在pub上查看其詳細信息。
本書后面**事件處理**一章中會實現一個全局事件總線。
## Flutter widget庫介紹
Flutter提供了一套豐富、強大的基礎widget,在基礎widget庫之上Flutter又提供了一套Material風格(Android默認的視覺風格)和一套Cupertino風格(iOS視覺風格)的widget庫。要使用基礎widget庫,需要先導入:
```
import 'package:flutter/widgets.dart';
```
下面我們介紹一下常用的widget。
### 基礎widget
- [`Text`](https://docs.flutter.io/flutter/widgets/Text-class.html):該 widget 可讓您創建一個帶格式的文本。
- [`Row`](https://docs.flutter.io/flutter/widgets/Row-class.html)、 [`Column`](https://docs.flutter.io/flutter/widgets/Column-class.html): 這些具有彈性空間的布局類Widget可讓您在水平(Row)和垂直(Column)方向上創建靈活的布局。其設計是基于web開發中的Flexbox布局模型。
- [`Stack`](https://docs.flutter.io/flutter/widgets/Stack-class.html): 取代線性布局 (譯者語:和Android中的FrameLayout相似),[`Stack`](https://docs.flutter.io/flutter/widgets/Stack-class.html)允許子 widget 堆疊, 你可以使用 [`Positioned`](https://docs.flutter.io/flutter/widgets/Positioned-class.html) 來定位他們相對于`Stack`的上下左右四條邊的位置。Stacks是基于Web開發中的絕對定位(absolute positioning )布局模型設計的。
- [`Container`](https://docs.flutter.io/flutter/widgets/Container-class.html): [`Container`](https://docs.flutter.io/flutter/widgets/Container-class.html) 可讓您創建矩形視覺元素。container 可以裝飾一個[`BoxDecoration`](https://docs.flutter.io/flutter/painting/BoxDecoration-class.html), 如 background、一個邊框、或者一個陰影。 [`Container`](https://docs.flutter.io/flutter/widgets/Container-class.html) 也可以具有邊距(margins)、填充(padding)和應用于其大小的約束(constraints)。另外, [`Container`](https://docs.flutter.io/flutter/widgets/Container-class.html)可以使用矩陣在三維空間中對其進行變換。
### Material widget
Flutter提供了一套豐富的Material widget,可幫助您構建遵循Material Design的應用程序。Material應用程序以[`MaterialApp`](https://docs.flutter.io/flutter/material/MaterialApp-class.html) widget開始, 該widget在應用程序的根部創建了一些有用的widget,比如一個Theme,它配置了應用的主題。 是否使用[`MaterialApp`](https://docs.flutter.io/flutter/material/MaterialApp-class.html)完全是可選的,但是使用它是一個很好的做法。在之前的示例中,我們已經使用過多個Material widget了,如:`Scaffold`、`AppBar`、`FlatButton`等。要使用Material widget,需要先引入它:
```
import 'package:flutter/material.dart';
```
### Cupertino widget
Flutter也提供了一套豐富的Cupertino風格的widget,盡管目前還沒有Material widget那么豐富,但也在不斷的完善中。值得一提的是在Material widget庫中,有一些widget可以根據實際運行平臺來切換表現風格,比如`MaterialPageRoute`,在路由切換時,如果是Android系統,它將會使用Android系統默認的頁面切換動畫(從底向上),如果是iOS系統時,它會使用iOS系統默認的頁面切換動畫(從右向左)。由于在前面的示例中還沒有Cupertino widget的示例,我們實現一個簡單的Cupertino頁面:
```
//導入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上頁面效果截圖:

### 總結
Flutter提供了豐富的widget,在實際的開發中你可以隨意使用它們,不要怕引入過多widget庫會讓你的應用安裝包變大,這不是web開發,dart在編譯時只會編譯你使用了的代碼。由于Material和Cupertino都是在基礎widget庫之上的,所以如果你的應用中引入了這兩者之一,則不需要再引入`flutter/widgets.dart`了,因為它們內部已經引入過了。
- 緣起
- 起步
- 移動開發技術簡介
- Flutter簡介
- 搭建Flutter開發環境
- 常見配置問題
- Dart語言簡介
- 第一個Flutter應用
- 計數器示例
- 路由管理
- 包管理
- 資源管理
- 調試Flutter APP
- Dart線程模型及異常捕獲
- 基礎Widgets
- Widget簡介
- 文本、字體樣式
- 按鈕
- 圖片和Icon
- 單選框和復選框
- 輸入框和表單
- 布局類Widgets
- 布局類Widgets簡介
- 線性布局Row、Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack、Positioned
- 容器類Widgets
- Padding
- 布局限制類容器ConstrainedBox、SizeBox
- 裝飾容器DecoratedBox
- 變換Transform
- Container容器
- Scaffold、TabBar、底部導航
- 可滾動Widgets
- 可滾動Widgets簡介
- SingleChildScrollView
- ListView
- GridView
- CustomScrollView
- 滾動監聽及控制ScrollController
- 功能型Widgets
- 導航返回攔截-WillPopScope
- 數據共享-InheritedWidget
- 主題-Theme
- 事件處理與通知
- 原始指針事件處理
- 手勢識別
- 全局事件總線
- 通知Notification
- 動畫
- Flutter動畫簡介
- 動畫結構
- 自定義路由過渡動畫
- Hero動畫
- 交錯動畫
- 自定義Widget
- 自定義Widget方法簡介
- 通過組合現有Widget實現
- 實例:TurnBox
- CustomPaint與Canvas
- 實例:圓形漸變進度條(自繪)
- 文件操作與網絡請求
- 文件操作
- Http請求-HttpClient
- Http請求-Dio package
- 實例:Http分塊下載
- WebSocket
- 使用Socket API
- Json轉Model
- 包與插件
- 開發package
- 插件開發:平臺通道簡介
- 插件開發:實現Android端API
- 插件開發:實現IOS端API
- 系統能力調用
- 國際化
- 讓App支持多語言
- 實現Localizations
- 使用Intl包
- Flutter核心原理
- Flutter UI系統
- Element和BuildContext
- RenderObject與RenderBox
- Flutter從啟動到顯示