Widgets概念
Flutter裏有一個非常重要的核心理念:一切皆爲組件,Flutter的所有元素都是由控件構成的。
與原生開發中控件所代表的含義不同,Flutter中widget的概念更加廣泛,它不僅可以表示UI元素,也可以表示一些功能性的組件,如用於手勢檢測的 GestureDetector widget、用於應用主題數據傳遞的Theme等等。而原生開發中的控件通常只是指UI元素。由於Flutter主要就是用於構建用戶界面的,所以,在大多數時候,我們可以簡單的認爲widget就是一個控件,不必糾結於概念。
Widget與Element
在正式介紹Flutter的Widget之前,我們需要理清兩個概念,即什麼是Widget,什麼是Element?
Widget的功能是“描述一個UI元素的配置數據,它就是說,Widget其實並不是表示最終繪製在設備屏幕上的顯示元素,而只是顯示元素的一個配置數據。實際上,Flutter中真正代表屏幕上顯示元素的類是Element,也就是說Widget只是描述Element的一個配置。並且一個Widget可以對應多個Element,這是因爲同一個Widget對象可以被添加到UI樹的不同部分,而真正渲染時,UI樹的每一個Widget節點都會對應一個Element對象。所以,理解Flutter的Widget需要理清兩個概念:
- Widget實際上就是Element的配置數據, Widget的功能是描述一個UI元素的一個配置數據, 而真正的UI渲染是由Element構成的。
- 由於Element是通過Widget生成,所以它們之間有對應關係,所以在大多數場景,我們可以簡單地認爲Widget就是指UI控件或UI渲染。
Widget聲明
首先,我們先來看一下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類的申明中,我們可以得到如下一些信息:
- Widget類繼承自DiagnosticableTree,主要作用是提供調試信息。
- Key: 這個key屬性類似於React/Vue中的key,主要的作用是決定是否在下一次build時複用舊的widget,決定的條件在canUpdate()方法中
- createElement():正如前文所述一個Widget可以對應多個Element;Flutter Framework在構建UI時,會先調用此方法生成對應節點的Element對象。此方法是Flutter Framework隱式調用的,在我們開發過程中基本不會調用到。
- debugFillProperties 複寫父類的方法,主要是設置DiagnosticableTree的一些特性。
- canUpdate()是一個靜態方法,它主要用於在Widget樹重新build時複用舊的widget。具體來說,是否使用新的Widget對象去更新舊UI樹上所對應的Element對象的配置;並且通過其源碼我們可以知道,只要newWidget與oldWidget的runtimeType和key同時相等時就會用newWidget去更新Element對象的配置,否則就會創建新的Element。
StatelessWidget
StatelessWidget是Flutter提供的一個不需要狀態更改的widget ,它沒有內部狀態管理功能。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),
),
);
}
}
按照慣例,widget的構造函數參數應使用命名參數,命名參數中的必要參數要添加@required標註,這樣有利於靜態代碼分析器進行檢查。另外,在繼承widget時,第一個參數通常應該是Key,另外,如果Widget需要接收子Widget,那麼child或children參數通常應被放在參數列表的最後。
然後,我們可以通過如下方式來使用Echo widget。
Widget build(BuildContext context) {
return Echo(text: "hello world");
}
運行後效果如下圖所示:
StatefulWidget
StatefulWidget 是一個可變狀態的widget。 使用setState方法管理StatefulWidget的狀態的改變。調用setState告訴Flutter框架,某個狀態發生了變化,Flutter會重新運行build方法,以便應用程序可以應用最新狀態。
和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實例。
StatelessWidget和StatefulWidget的區別
通過上面的講解,我們可以得出如下結論:
- StatelessWidget是狀態不可變的widget, 初始狀態設置以後就不可再變化, 如果需要變化需要重新創建StatefulWidget,因爲StatefulWidget可以保存自己的狀態。
- 在Flutter中通過引入State來保存狀態, 當State的狀態改變時,能重新構建本節點以及孩子的Widget樹來進行UI變化。
- 如果需要主動改變State的狀態,需要通過setState()方法進行觸發,單純改變數據是不會引發UI改變的
Widgets的State
說到組件,就不得不提到Widgets的State。通常,一個StatefulWidget類會對應一個State類,State表示與其對應的StatefulWidget要維護的狀態,State中的保存的狀態信息有如下兩個作用:
- 在widget build時可以被同步讀取。
- 在widget生命週期中可以被改變,當State被改變時,可以手動調用其setState()方法通知Flutter framework狀態發生改變,Flutter framework在收到消息後,會重新調用其build方法重新構建widget樹,從而達到更新UI的目的。
State有兩個常用屬性:widget和context。
- widget:它表示與該State實例關聯的widget實例,由Flutter framework動態設置。注意,這種關聯並非永久的,因爲在應用聲明週期中,UI樹上的某一個節點的widget實例在重新構建時可能會變化,但State實例只會在第一次插入到樹中時被創建,當在重新構建時,如果widget被修改了,Flutter framework會動態設置State.widget爲新的widget實例。
- context,它是BuildContext類的一個實例,表示構建widget的上下文,它是操作widget在樹中位置的一個句柄,它包含了一些查找、遍歷當前Widget樹的一些方法。每一個widget都有一個自己的context對象。
生命週期
和原生平臺的控件一樣,State也有自己的生命週期。爲了加深讀者對State生命週期的印象,本節我們通過一個實例來演示一下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整型參數,它表示計數器的初始值。接下來,我們看一下_CounterWidgetState的實現:
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 Scaffold(
body: 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會依次被調用。
通過上面的示例,我們將StatefulWidget生命週期整理如下圖:
StatefulWidget的生命週期大致可分爲三個階段:
- 初始化:插入渲染樹,這一階段涉及的生命週期函數主要有createState、initState、didChangeDependencies和build。
- 運行中:在渲染樹中存在,這一階段涉及的生命週期函數主要有didUpdateWidget和build。
- 銷燬:從渲染樹中移除,此階段涉及的生命週期函數主要有deactivate和dispose。
初始化階段
createState:createState必須且僅執行一次,它用來創建state,當創建StatefulWidget時,該放方法就會被執行。
initState:在創建StatefulWidget後,initState是第一個被調用的方法,同createState一樣只被調用一次,此時widget的被添加至渲染樹,mount的值會變爲true,但並沒有渲染。我們可以在該方法內做一些初始化操作。
didChangeDependencies:當widget第一次被創建時,didChangeDependencies緊跟着initState函數之後調用,在widget刷新時,該方法不會被調用。它會在“依賴”發生變化時被Flutter Framework調用,這個依賴是指widget是否使用父widget中InheritedWidget的數據。也即是只有在widget依賴的InheritedWidget發生變化之後,didChangeDependencies纔會調用。
這種機制可以使子組件在所依賴的InheritedWidget變化時來更新自身!比如當主題、locale(語言)等發生變化時,依賴其的子widget的didChangeDependencies方法將會被調用。
build:build會在widget第一次創建時緊跟着didChangeDependencies方法之後和UI重新渲染時被調用。build只做widget的創建操作,如果在build裏做其他操作,會影響UI的渲染效果。
運行中
StatefulWidget運行中只會調用兩個函數,即didUpdateWidget和build。
didUpdateWidget:當組件的狀態改變的時候就會調用didUpdateWidget,比如調用了setState。
銷燬
deactivate:當State對象從樹中被移除時,會調用此回調函數,這標誌着 StatefulWidget將要執行銷燬操作。頁面切換時,也會調用它,因爲此時State在視圖樹中的位置發生了變化但是State不會被銷燬,而是重新插入到渲染樹中。 重寫的時候必須要調用 super.deactivate()。
dispose:從渲染樹中移除時調用,State會永久的從渲染樹中移除,和initState正好相反mount值變味false。這時候就可以在dispose裏做一些取消監聽操作。
爲了方便讀者理解,我們看一下StatefulWidget的生命週期函數調用情況。
生命週期 | 調用次數 | 調用時間 |
---|---|---|
createState | 1 | 組件創建時 |
initState | 1 | 組件創建時 |
didChangeDependencies | n | 組件創建或狀態發生變化 |
build | n | 組件創建或UI重新渲染 |
didUpdateWidget | n | 組件創建或UI重新渲染 |
deactivate | n | State對象將要移除時 |
dispose | 1 | state對象被銷燬 |
內置組件庫
Flutter SDK提供了一套豐富、強大的基礎組件,在基礎組件庫之上Flutter又提供了一套Material風格(Android默認的視覺風格)和一套Cupertino風格(iOS視覺風格)的組件庫。使用前只需要導入即可使用:
import 'package:flutter/widgets.dart';
基礎組件
Flutter SDK提供了很多功能豐富的基礎組件,常見的有如下一些:
- Text:該組件可讓您創建一個帶格式的文本。
- Row、 Column: 這些具有彈性空間的佈局類Widget可讓您在水平(Row)和垂直(Column)方向上創建靈活的佈局。其設計是基於Web開發中的Flexbox佈局模型。
- Stack: 取代線性佈局 (譯者語:和Android中的FrameLayout相似),Stack允許子 widget 堆疊, 你可以使用 Positioned 來定位他們相對於Stack的上下左右四條邊的位置。Stacks是基於Web開發中的絕對定位(absolute positioning )佈局模型設計的。
- Container: Container 可讓您創建矩形視覺元素。container 可以裝飾一個BoxDecoration, 如 background、一個邊框、或者一個陰影。 Container 也可以具有邊距(margins)、填充(padding)和應用於其大小的約束(constraints)。另外, Container可以使用矩陣在三維空間中對其進行變換。
Material組件
衆所周知,Material是Android應用默認的視覺風格,Cupertino則是iOS應用的默認視覺風格,爲了實現兩種不同的視覺風格,Flutter 在基礎組件庫之上Flutter又提供了一套Material風格和一套Cupertino風格的組件庫,以滿足兩種不同設計風格的開發需要。
Material應用程序以MaterialApp 組件開始, 該組件在應用程序的根部創建了一些必要的組件,比如Theme組件,它用於配置應用的主題。 是否使用MaterialApp完全是可選的,但是使用它是一個很好的做法。在之前的示例中,我們已經使用過多個Material 組件了,如:Scaffold、AppBar、FlatButton等。
要使用Material 組件,需要先引入它:
import 'package:flutter/material.dart';
Cupertino組件
Flutter也提供了一套豐富的Cupertino風格的組件,儘管目前還沒有Material 組件那麼豐富,但是它仍在不斷的完善中。目前,Flutter提供的Cupertino組件主要有 CupertinoTabBar、 CupertinoActivityIndicator、CupertinoPageScaffold、 CupertinoTabScaffold、 CupertinoTabView 等 。
關於Cupertino組件,大家可以參考官方的介紹:Cupertino (iOS風格) Widgets