1.1 Widget 概念
我們知道在Flutter中幾乎所有的對象都是一個 widget 。與原生開發(fā)中“控件”不同的是,F(xiàn)lutter 中的 widget 的概念更廣泛,它不僅可以表示UI元素,也可以表示一些功能性的組件如:用于手勢檢測的 GestureDetector 、用于APP主題數(shù)據(jù)傳遞的 Theme 等等,而原生開發(fā)中的控件通常只是指UI元素。在后面的內(nèi)容中,我們在描述UI元素時可能會用到“控件”、“組件”這樣的概念,讀者心里需要知道他們就是 widget ,只是在不同場景的不同表述而已。由于 Flutter 主要就是用于構(gòu)建用戶界面的,所以,在大多數(shù)時候,可以認為 widget 就是一個控件,不必糾結(jié)于概念。
Flutter 中是通過 Widget 嵌套 Widget 的方式來構(gòu)建UI和進行實踐處理的,所以記住,F(xiàn)lutter 中萬物皆為Widget。
1.2 Flutter中的四棵樹
既然 Widget 只是描述一個UI元素的配置信息,那么真正的布局、繪制是由誰來完成的呢?Flutter 框架的的處理流程是這樣的:
- 根據(jù) Widget 樹生成一個 Element 樹,Element 樹中的節(jié)點都繼承自
Element類。 - 根據(jù) Element 樹生成 Render 樹(渲染樹),渲染樹中的節(jié)點都繼承自
RenderObject類。 - 根據(jù)渲染樹生成 Layer 樹,然后上屏顯示,Layer 樹中的節(jié)點都繼承自
Layer類。
真正的布局和渲染邏輯在 Render 樹中,Element 是 Widget 和 RenderObject 的粘合劑,可以理解為一個中間代理。我們通過一個例子來說明,假設(shè)有如下 Widget 樹:
Container( // 一個容器 widget
color: Colors.blue, // 設(shè)置容器背景色
child: Row( // 可以將子widget沿水平方向排列
children: [
Image.network('https://www.example.com/1.png'), // 顯示圖片的 widget
const Text('A'),
],
),
);
注意,如果 Container 設(shè)置了背景色,Container 內(nèi)部會創(chuàng)建一個新的 ColoredBox 來填充背景,相關(guān)邏輯如下:
if (color != null)
current = ColoredBox(color: color!, child: current);
而 Image 內(nèi)部會通過 RawImage 來渲染圖片、Text 內(nèi)部會通過 RichText 來渲染文本,所以最終的 Widget樹、Element 樹、渲染樹結(jié)構(gòu)如下:
這里需要注意:
- 三棵樹中,Widget 和 Element 是一一對應(yīng)的,但并不和 RenderObject 一一對應(yīng)。比如
StatelessWidget和StatefulWidget都沒有對應(yīng)的 RenderObject。 - 渲染樹在上屏前會生成一棵 Layer 樹。
1.3 StatelessWidget
StatelessWidget 無狀態(tài)的Widget
相對比較簡單,它繼承自widget類,重寫了createElement()方法:
@override
StatelessElement createElement() => StatelessElement(this);
StatelessWidget用于不需要維護狀態(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關(guān)鍵字,這樣有利于靜態(tài)代碼分析器進行檢查;在繼承 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");
}
Context
build方法有一個context參數(shù),它是BuildContext類的一個實例,表示當前 widget 在 widget 樹中的上下文,每一個 widget 都會對應(yīng)一個 context 對象(因為每一個 widget 都是 widget 樹上的一個節(jié)點)。實際上,context是當前 widget 在 widget 樹中位置中執(zhí)行”相關(guān)操作“的一個句柄(handle),比如它提供了從當前 widget 開始向上遍歷 widget 樹以及按照 widget 類型查找父級 widget 的方法。
1.4 StatefulWidget
StatefulWidget 有狀態(tài)的Widget
和StatelessWidget一樣,StatefulWidget也是繼承自widget類,并重寫了createElement()方法,不同的是返回的Element 對象并不相同;另外StatefulWidget類中添加了一個新的接口createState()。
下面我們看看StatefulWidget的類定義:
abstract class StatefulWidget extends Widget {
const StatefulWidget({ Key key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
State createState();
}
-
StatefulElement間接繼承自Element類,與StatefulWidget相對應(yīng)(作為其配置數(shù)據(jù))。StatefulElement中可能會多次調(diào)用createState()來創(chuàng)建狀態(tài)(State)對象。 -
createState()用于創(chuàng)建和 StatefulWidget 相關(guān)的狀態(tài),它在StatefulWidget 的生命周期中可能會被多次調(diào)用。例如,當一個 StatefulWidget 同時插入到 widget 樹的多個位置時,F(xiàn)lutter 框架就會調(diào)用該方法為每一個位置生成一個獨立的State實例,其實,本質(zhì)上就是一個StatefulElement對應(yīng)一個State實例。
1.5 State
一個 StatefulWidget 類會對應(yīng)一個 State 類,State表示與其對應(yīng)的 StatefulWidget 要維護的狀態(tài),State 中的保存的狀態(tài)信息可以:
- 在 widget 構(gòu)建時可以被同步讀取。
- 在 widget 生命周期中可以被改變,當State被改變時,可以手動調(diào)用其
setState()方法通知Flutter 框架狀態(tài)發(fā)生改變,F(xiàn)lutter 框架在收到消息后,會重新調(diào)用其build方法重新構(gòu)建 widget 樹,從而達到更新UI的目的。
State 中有兩個常用屬性:
-
widget,它表示與該 State 實例關(guān)聯(lián)的 widget 實例,由Flutter 框架動態(tài)設(shè)置。注意,這種關(guān)聯(lián)并非永久的,因為在應(yīng)用生命周期中,UI樹上的某一個節(jié)點的 widget 實例在重新構(gòu)建時可能會變化,但State實例只會在第一次插入到樹中時被創(chuàng)建,當在重新構(gòu)建時,如果 widget 被修改了,F(xiàn)lutter 框架會動態(tài)設(shè)置State. widget 為新的 widget 實例。 -
context。StatefulWidget對應(yīng)的 BuildContext,作用同StatelessWidget 的BuildContext。
State生命周期
理解State的生命周期對flutter開發(fā)非常重要,為了加深讀者印象,本節(jié)我們通過一個實例來演示一下 State 的生命周期。在接下來的示例中,我們?nèi)匀灰杂嫈?shù)器功能為例,實現(xiàn)一個計數(shù)器 CounterWidget 組件 ,點擊它可以使計數(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() => _CounterWidgetState();
}
CounterWidget接收一個initValue整型參數(shù),它表示計數(shù)器的初始值。下面我們看一下State的代碼:
class _CounterWidgetState extends State<CounterWidget> {
int _counter = 0;
@override
void initState() {
super.initState();
//初始化狀態(tài) widget.initValue 獲取CounterWidget initValue;
_counter = widget.initValue;
print("initState");
}
@override
Widget build(BuildContext context) {
print("build");
return Scaffold(
body: Center(
child: TextButton(
child: Text('$_counter'),
//點擊后計數(shù)器自增
onPressed: () => setState(
() => ++_counter,
),
),
),
);
}
@override
void didUpdateWidget(CounterWidget oldWidget) {
super.didUpdateWidget(oldWidget);
print("didUpdateWidget ");
}
@override
void deactivate() {
super.deactivate();
print("deactivate");
}
@override
void dispose() {
super.dispose();
print("dispose");
}
@override
void reassemble() {
super.reassemble();
print("reassemble");
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
print("didChangeDependencies");
}
}
接下來,我們創(chuàng)建一個新路由,在新路由中,我們只顯示一個CounterWidget:
class StateLifecycleTest extends StatelessWidget {
const StateLifecycleTest({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return CounterWidget();
}
}
我們運行應(yīng)用并打開該路由頁面,在新路由頁打開后,屏幕中央就會出現(xiàn)一個數(shù)字0,然后控制臺日志輸出:
I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build
可以看到,在StatefulWidget插入到 widget 樹時首先initState方法會被調(diào)用。
然后我們點擊??按鈕熱重載,控制臺輸出日志如下:
可以看到,在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,將 StateLifecycleTest 的 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)用。
StatefulWidget 生命周期如圖3-2所示:
-
initState:當 widget 第一次插入到 widget 樹時會被調(diào)用,對于每一個State對象,F(xiàn)lutter 框架只會調(diào)用一次該回調(diào),所以,通常在該回調(diào)中做一些一次性的操作,如狀態(tài)初始化、訂閱子樹的事件通知等。不能在該回調(diào)中調(diào)用BuildContext.dependOnInheritedWidgetOfExactType(該方法用于在 widget 樹上獲取離當前 widget 最近的一個父級InheritedWidget,關(guān)于InheritedWidget我們將在后面章節(jié)介紹),原因是在初始化完成后, widget 樹中的InheritFrom widget也可能會發(fā)生變化,所以正確的做法應(yīng)該在在build()方法或didChangeDependencies()中調(diào)用它。 -
didChangeDependencies():當State對象的依賴發(fā)生變化時會被調(diào)用;例如:在之前build()中包含了一個InheritedWidget,然后在之后的build()中Inherited widget發(fā)生了變化,那么此時Inherited widget的子 widget 的didChangeDependencies()回調(diào)都會被調(diào)用。典型的場景是當系統(tǒng)語言 Locale 或應(yīng)用主題改變時,F(xiàn)lutter 框架會通知 widget 調(diào)用此回調(diào)。 -
build():此回調(diào)讀者現(xiàn)在應(yīng)該已經(jīng)相當熟悉了,它主要是用于構(gòu)建 widget 子樹的,會在如下場景被調(diào)用:- 在調(diào)用
initState()之后。 - 在調(diào)用
didUpdateWidget()之后。 - 在調(diào)用
setState()之后。 - 在調(diào)用
didChangeDependencies()之后。 - 在State對象從樹中一個位置移除后(會調(diào)用deactivate)又重新插入到樹的其它位置之后。
- 在調(diào)用
-
reassemble():此回調(diào)是專門為了開發(fā)調(diào)試而提供的,在熱重載(hot reload)時會被調(diào)用,此回調(diào)在Release模式下永遠不會被調(diào)用。 -
didUpdateWidget ():在 widget 重新構(gòu)建時,F(xiàn)lutter 框架會調(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():當 State 對象從樹中被移除時,會調(diào)用此回調(diào)。在一些場景下,F(xiàn)lutter 框架會將 State 對象重新插到樹中,如包含此 State 對象的子樹在樹的一個位置移動到另一個位置時(可以通過GlobalKey 來實現(xiàn))。如果移除后沒有重新插入到樹中則緊接著會調(diào)用dispose()方法。 -
dispose():當 State 對象從樹中被永久移除時調(diào)用;通常在此回調(diào)中釋放資源。
1.6 在 widget 樹中獲取State對象
由于 StatefulWidget 的的具體邏輯都在其 State 中,所以很多時候,我們需要獲取 StatefulWidget 對應(yīng)的State 對象來調(diào)用一些方法,比如Scaffold組件對應(yīng)的狀態(tài)類ScaffoldState中就定義了打開 SnackBar(路由頁底部提示條)的方法。我們有兩種方法在子 widget 樹中獲取父級 StatefulWidget 的State 對象
通過Context獲取
context對象有一個findAncestorStateOfType()方法,該方法可以從當前節(jié)點沿著 widget 樹向上查找指定類型的 StatefulWidget 對應(yīng)的 State 對象。下面是實現(xiàn)打開 SnackBar 的示例:
class GetStateObjectRoute extends StatefulWidget {
const GetStateObjectRoute({Key? key}) : super(key: key);
@override
State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}
class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("子樹中獲取State對象"),
),
body: Center(
child: Column(
children: [
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
// 查找父級最近的Scaffold對應(yīng)的ScaffoldState對象
ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
// 打開抽屜菜單
_state.openDrawer();
},
child: Text('打開抽屜菜單1'),
);
}),
],
),
),
drawer: Drawer(),
);
}
}
一般來說,如果 StatefulWidget 的狀態(tài)是私有的(不應(yīng)該向外部暴露),那么我們代碼中就不應(yīng)該去直接獲取其 State 對象;如果StatefulWidget的狀態(tài)是希望暴露出的(通常還有一些組件的操作方法),我們則可以去直接獲取其State對象。但是通過 context.findAncestorStateOfType 獲取 StatefulWidget 的狀態(tài)的方法是通用的,我們并不能在語法層面指定 StatefulWidget 的狀態(tài)是否私有,所以在 Flutter 開發(fā)中便有了一個默認的約定:如果 StatefulWidget 的狀態(tài)是希望暴露出的,應(yīng)當在 StatefulWidget 中提供一個of 靜態(tài)方法來獲取其 State 對象,開發(fā)者便可直接通過該方法來獲??;如果 State不希望暴露,則不提供of方法。這個約定在 Flutter SDK 里隨處可見。所以,上面示例中的Scaffold也提供了一個of方法,我們其實是可以直接調(diào)用它的
比如我們想顯示 snack bar 的話可以通過下面代碼調(diào)用:
Builder(builder: (context) {
return ElevatedButton(
onPressed: () {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text("我是SnackBar")),
);
},
child: Text('顯示SnackBar'),
);
}),
通過GlobalKey
Flutter還有一種通用的獲取State對象的方法——通過GlobalKey來獲取! 步驟分兩步:
-
給目標
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對象,如果當前 widget 是StatefulWidget,則可以通過globalKey.currentState來獲得該 widget 對應(yīng)的state對象
注意:使用 GlobalKey 開銷較大,如果有其他可選方案,應(yīng)盡量避免使用它。另外,同一個 GlobalKey 在整個 widget 樹中必須是唯一的,不能重復(fù)
1.7 通過 RenderObject 自定義 Widget
StatelessWidget 和 StatefulWidget 都是用于組合其它組件的,它們本身沒有對應(yīng)的 RenderObject。Flutter 組件庫中的很多基礎(chǔ)組件都不是通過StatelessWidget 和 StatefulWidget 來實現(xiàn)的,比如 Text 、Column、Align等,就好比搭積木,StatelessWidget 和 StatefulWidget 可以將積木搭成不同的樣子,但前提是得有積木,而這些積木都是通過自定義 RenderObject 來實現(xiàn)的。實際上Flutter 最原始的定義組件的方式就是通過定義RenderObject 來實現(xiàn),而StatelessWidget 和 StatefulWidget 只是提供的兩個幫助類。我們簡單演示一下通過RenderObject定義組件的方式:
class CustomWidget extends LeafRenderObjectWidget{
@override
RenderObject createRenderObject(BuildContext context) {
// 創(chuàng)建 RenderObject
return RenderCustomObject();
}
@override
void updateRenderObject(BuildContext context, RenderCustomObject renderObject) {
// 更新 RenderObject
super.updateRenderObject(context, renderObject);
}
}
class RenderCustomObject extends RenderBox{
@override
void performLayout() {
// 實現(xiàn)布局邏輯
}
@override
void paint(PaintingContext context, Offset offset) {
// 實現(xiàn)繪制
}
}
如果組件不會包含子組件,則我們可以直接繼承自 LeafRenderObjectWidget ,它是 RenderObjectWidget 的子類,而 RenderObjectWidget 繼承自 Widget ,我們可以看一下它的實現(xiàn):
abstract class LeafRenderObjectWidget extends RenderObjectWidget {
const LeafRenderObjectWidget({ Key? key }) : super(key: key);
@override
LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}
很簡單,就是幫 widget 實現(xiàn)了createElement 方法,它會為組件創(chuàng)建一個 類型為 LeafRenderObjectElement 的 Element對象。如果自定義的 widget 可以包含子組件,則可以根據(jù)子組件的數(shù)量來選擇繼承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget,它們也實現(xiàn)了createElement() 方法,返回不同類型的 Element 對象。