1.1 Widget 概念
我們知道在Flutter中幾乎所有的對象都是一個(gè) widget 嗦明。與原生開發(fā)中“控件”不同的是箩做,F(xiàn)lutter 中的 widget 的概念更廣泛项玛,它不僅可以表示UI元素势似,也可以表示一些功能性的組件如:用于手勢檢測的 GestureDetector
桅滋、用于APP主題數(shù)據(jù)傳遞的 Theme
等等,而原生開發(fā)中的控件通常只是指UI元素既绩。在后面的內(nèi)容中概龄,我們在描述UI元素時(shí)可能會(huì)用到“控件”、“組件”這樣的概念饲握,讀者心里需要知道他們就是 widget 私杜,只是在不同場景的不同表述而已。由于 Flutter 主要就是用于構(gòu)建用戶界面的救欧,所以衰粹,在大多數(shù)時(shí)候,可以認(rèn)為 widget 就是一個(gè)控件笆怠,不必糾結(jié)于概念铝耻。
Flutter 中是通過 Widget 嵌套 Widget 的方式來構(gòu)建UI和進(jìn)行實(shí)踐處理的,所以記住蹬刷,F(xiàn)lutter 中萬物皆為Widget瓢捉。
1.2 Flutter中的四棵樹
既然 Widget 只是描述一個(gè)UI元素的配置信息,那么真正的布局箍铭、繪制是由誰來完成的呢泊柬?Flutter 框架的的處理流程是這樣的:
- 根據(jù) Widget 樹生成一個(gè) Element 樹,Element 樹中的節(jié)點(diǎn)都繼承自
Element
類诈火。 - 根據(jù) Element 樹生成 Render 樹(渲染樹)兽赁,渲染樹中的節(jié)點(diǎn)都繼承自
RenderObject
類。 - 根據(jù)渲染樹生成 Layer 樹冷守,然后上屏顯示刀崖,Layer 樹中的節(jié)點(diǎn)都繼承自
Layer
類。
真正的布局和渲染邏輯在 Render 樹中拍摇,Element 是 Widget 和 RenderObject 的粘合劑亮钦,可以理解為一個(gè)中間代理。我們通過一個(gè)例子來說明充活,假設(shè)有如下 Widget 樹:
Container( // 一個(gè)容器 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)部會(huì)創(chuàng)建一個(gè)新的 ColoredBox 來填充背景,相關(guān)邏輯如下:
if (color != null)
current = ColoredBox(color: color!, child: current);
而 Image 內(nèi)部會(huì)通過 RawImage 來渲染圖片映穗、Text 內(nèi)部會(huì)通過 RichText 來渲染文本窖张,所以最終的 Widget樹、Element 樹蚁滋、渲染樹結(jié)構(gòu)如下:
這里需要注意:
- 三棵樹中宿接,Widget 和 Element 是一一對應(yīng)的,但并不和 RenderObject 一一對應(yīng)辕录。比如
StatelessWidget
和StatefulWidget
都沒有對應(yīng)的 RenderObject睦霎。 - 渲染樹在上屏前會(huì)生成一棵 Layer 樹。
1.3 StatelessWidget
StatelessWidget
無狀態(tài)的Widget
相對比較簡單走诞,它繼承自widget
類副女,重寫了createElement()
方法:
@override
StatelessElement createElement() => StatelessElement(this);
StatelessWidget
用于不需要維護(hù)狀態(tài)的場景地消,它通常在build
方法中通過嵌套其它 widget 來構(gòu)建UI埂息,在構(gòu)建過程中會(huì)遞歸的構(gòu)建其嵌套的 widget 。我們看一個(gè)簡單的例子
class Echo extends StatelessWidget {
const Echo({
Key? key,
required this.text,
this.backgroundColor = Colors.grey, //默認(rèn)為灰色
}):super(key:key);
final String text;
final Color backgroundColor;
@override
widget build(BuildContext context) {
return Center(
child: Container(
color: backgroundColor,
child: Text(text),
),
);
}
}
上面的代碼梯澜,實(shí)現(xiàn)了一個(gè)回顯字符串的Echo
widget 姻锁。
按照慣例,widget 的構(gòu)造函數(shù)參數(shù)應(yīng)使用命名參數(shù)猜欺,命名參數(shù)中的必需要傳的參數(shù)要添加
required
關(guān)鍵字位隶,這樣有利于靜態(tài)代碼分析器進(jìn)行檢查;在繼承 widget 時(shí)开皿,第一個(gè)參數(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
方法有一個(gè)context
參數(shù),它是BuildContext
類的一個(gè)實(shí)例嫉你,表示當(dāng)前 widget 在 widget 樹中的上下文月帝,每一個(gè) widget 都會(huì)對應(yīng)一個(gè) context 對象(因?yàn)槊恳粋€(gè) widget 都是 widget 樹上的一個(gè)節(jié)點(diǎn))。實(shí)際上幽污,context
是當(dāng)前 widget 在 widget 樹中位置中執(zhí)行”相關(guān)操作“的一個(gè)句柄(handle)嚷辅,比如它提供了從當(dāng)前 widget 開始向上遍歷 widget 樹以及按照 widget 類型查找父級 widget 的方法。
1.4 StatefulWidget
StatefulWidget 有狀態(tài)的Widget
和StatelessWidget
一樣距误,StatefulWidget
也是繼承自widget
類簸搞,并重寫了createElement()
方法扁位,不同的是返回的Element
對象并不相同;另外StatefulWidget
類中添加了一個(gè)新的接口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
中可能會(huì)多次調(diào)用createState()
來創(chuàng)建狀態(tài)(State)對象则酝。 -
createState()
用于創(chuàng)建和 StatefulWidget 相關(guān)的狀態(tài)殉簸,它在StatefulWidget 的生命周期中可能會(huì)被多次調(diào)用。例如沽讹,當(dāng)一個(gè) StatefulWidget 同時(shí)插入到 widget 樹的多個(gè)位置時(shí)般卑,F(xiàn)lutter 框架就會(huì)調(diào)用該方法為每一個(gè)位置生成一個(gè)獨(dú)立的State實(shí)例,其實(shí)爽雄,本質(zhì)上就是一個(gè)StatefulElement
對應(yīng)一個(gè)State實(shí)例蝠检。
1.5 State
一個(gè) StatefulWidget 類會(huì)對應(yīng)一個(gè) State 類,State表示與其對應(yīng)的 StatefulWidget 要維護(hù)的狀態(tài)挚瘟,State 中的保存的狀態(tài)信息可以:
- 在 widget 構(gòu)建時(shí)可以被同步讀取叹谁。
- 在 widget 生命周期中可以被改變,當(dāng)State被改變時(shí)乘盖,可以手動(dòng)調(diào)用其
setState()
方法通知Flutter 框架狀態(tài)發(fā)生改變焰檩,F(xiàn)lutter 框架在收到消息后,會(huì)重新調(diào)用其build
方法重新構(gòu)建 widget 樹订框,從而達(dá)到更新UI的目的析苫。
State 中有兩個(gè)常用屬性:
-
widget
,它表示與該 State 實(shí)例關(guān)聯(lián)的 widget 實(shí)例穿扳,由Flutter 框架動(dòng)態(tài)設(shè)置衩侥。注意,這種關(guān)聯(lián)并非永久的矛物,因?yàn)樵趹?yīng)用生命周期中茫死,UI樹上的某一個(gè)節(jié)點(diǎn)的 widget 實(shí)例在重新構(gòu)建時(shí)可能會(huì)變化,但State實(shí)例只會(huì)在第一次插入到樹中時(shí)被創(chuàng)建履羞,當(dāng)在重新構(gòu)建時(shí)峦萎,如果 widget 被修改了,F(xiàn)lutter 框架會(huì)動(dòng)態(tài)設(shè)置State. widget 為新的 widget 實(shí)例吧雹。 -
context
骨杂。StatefulWidget對應(yīng)的 BuildContext,作用同StatelessWidget 的BuildContext雄卷。
State生命周期
理解State的生命周期對flutter開發(fā)非常重要搓蚪,為了加深讀者印象,本節(jié)我們通過一個(gè)實(shí)例來演示一下 State 的生命周期丁鹉。在接下來的示例中妒潭,我們?nèi)匀灰杂?jì)數(shù)器功能為例悴能,實(shí)現(xiàn)一個(gè)計(jì)數(shù)器 CounterWidget 組件 ,點(diǎn)擊它可以使計(jì)數(shù)器加1雳灾,由于要保存計(jì)數(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
接收一個(gè)initValue
整型參數(shù)谎亩,它表示計(jì)數(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'),
//點(diǎn)擊后計(jì)數(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)建一個(gè)新路由匈庭,在新路由中夫凸,我們只顯示一個(gè)CounterWidget
:
class StateLifecycleTest extends StatelessWidget {
const StateLifecycleTest({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return CounterWidget();
}
}
我們運(yùn)行應(yīng)用并打開該路由頁面,在新路由頁打開后阱持,屏幕中央就會(huì)出現(xiàn)一個(gè)數(shù)字0夭拌,然后控制臺(tái)日志輸出:
I/flutter ( 5436): initState
I/flutter ( 5436): didChangeDependencies
I/flutter ( 5436): build
可以看到,在StatefulWidget插入到 widget 樹時(shí)首先initState
方法會(huì)被調(diào)用衷咽。
然后我們點(diǎn)擊??按鈕熱重載鸽扁,控制臺(tái)輸出日志如下:
可以看到,在StatefulWidget插入到 widget 樹時(shí)首先initState
方法會(huì)被調(diào)用镶骗。
然后我們點(diǎn)擊??按鈕熱重載桶现,控制臺(tái)輸出日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): didUpdateWidget
I/flutter ( 5436): build
可以看到此時(shí)initState
和didChangeDependencies
都沒有被調(diào)用,而此時(shí)didUpdateWidget
被調(diào)用鼎姊。
接下來巩那,我們在 widget 樹中移除CounterWidget
,將 StateLifecycleTest 的 build
方法改為:
Widget build(BuildContext context) {
//移除計(jì)數(shù)器
//return CounterWidget ();
//隨便返回一個(gè)Text()
return Text("xxx");
}
然后熱重載此蜈,日志如下:
I/flutter ( 5436): reassemble
I/flutter ( 5436): deactive
I/flutter ( 5436): dispose
我們可以看到,在CounterWidget
從 widget 樹中移除時(shí)噪生,deactive
和dispose
會(huì)依次被調(diào)用裆赵。
StatefulWidget 生命周期如圖3-2所示:
-
initState
:當(dāng) widget 第一次插入到 widget 樹時(shí)會(huì)被調(diào)用,對于每一個(gè)State對象跺嗽,F(xiàn)lutter 框架只會(huì)調(diào)用一次該回調(diào)战授,所以,通常在該回調(diào)中做一些一次性的操作桨嫁,如狀態(tài)初始化植兰、訂閱子樹的事件通知等。不能在該回調(diào)中調(diào)用BuildContext.dependOnInheritedWidgetOfExactType
(該方法用于在 widget 樹上獲取離當(dāng)前 widget 最近的一個(gè)父級InheritedWidget
璃吧,關(guān)于InheritedWidget
我們將在后面章節(jié)介紹)楣导,原因是在初始化完成后, widget 樹中的InheritFrom widget
也可能會(huì)發(fā)生變化畜挨,所以正確的做法應(yīng)該在在build()
方法或didChangeDependencies()
中調(diào)用它筒繁。 -
didChangeDependencies()
:當(dāng)State對象的依賴發(fā)生變化時(shí)會(huì)被調(diào)用噩凹;例如:在之前build()
中包含了一個(gè)InheritedWidget
,然后在之后的build()
中Inherited widget
發(fā)生了變化毡咏,那么此時(shí)Inherited widget
的子 widget 的didChangeDependencies()
回調(diào)都會(huì)被調(diào)用驮宴。典型的場景是當(dāng)系統(tǒng)語言 Locale 或應(yīng)用主題改變時(shí),F(xiàn)lutter 框架會(huì)通知 widget 調(diào)用此回調(diào)呕缭。 -
build()
:此回調(diào)讀者現(xiàn)在應(yīng)該已經(jīng)相當(dāng)熟悉了堵泽,它主要是用于構(gòu)建 widget 子樹的,會(huì)在如下場景被調(diào)用:- 在調(diào)用
initState()
之后恢总。 - 在調(diào)用
didUpdateWidget()
之后迎罗。 - 在調(diào)用
setState()
之后。 - 在調(diào)用
didChangeDependencies()
之后离熏。 - 在State對象從樹中一個(gè)位置移除后(會(huì)調(diào)用deactivate)又重新插入到樹的其它位置之后佳谦。
- 在調(diào)用
-
reassemble()
:此回調(diào)是專門為了開發(fā)調(diào)試而提供的,在熱重載(hot reload)時(shí)會(huì)被調(diào)用滋戳,此回調(diào)在Release模式下永遠(yuǎn)不會(huì)被調(diào)用钻蔑。 -
didUpdateWidget ()
:在 widget 重新構(gòu)建時(shí),F(xiàn)lutter 框架會(huì)調(diào)用widget.canUpdate
來檢測 widget 樹中同一位置的新舊節(jié)點(diǎn)奸鸯,然后決定是否需要更新咪笑,如果widget.canUpdate
返回true
則會(huì)調(diào)用此回調(diào)。正如之前所述娄涩,widget.canUpdate
會(huì)在新舊 widget 的key
和runtimeType
同時(shí)相等時(shí)會(huì)返回true窗怒,也就是說在在新舊 widget 的key和runtimeType同時(shí)相等時(shí)didUpdateWidget()
就會(huì)被調(diào)用。 -
deactivate()
:當(dāng) State 對象從樹中被移除時(shí)蓄拣,會(huì)調(diào)用此回調(diào)扬虚。在一些場景下,F(xiàn)lutter 框架會(huì)將 State 對象重新插到樹中球恤,如包含此 State 對象的子樹在樹的一個(gè)位置移動(dòng)到另一個(gè)位置時(shí)(可以通過GlobalKey 來實(shí)現(xiàn))辜昵。如果移除后沒有重新插入到樹中則緊接著會(huì)調(diào)用dispose()
方法。 -
dispose()
:當(dāng) State 對象從樹中被永久移除時(shí)調(diào)用咽斧;通常在此回調(diào)中釋放資源堪置。
1.6 在 widget 樹中獲取State對象
由于 StatefulWidget 的的具體邏輯都在其 State 中,所以很多時(shí)候张惹,我們需要獲取 StatefulWidget 對應(yīng)的State 對象來調(diào)用一些方法舀锨,比如Scaffold
組件對應(yīng)的狀態(tài)類ScaffoldState
中就定義了打開 SnackBar(路由頁底部提示條)的方法。我們有兩種方法在子 widget 樹中獲取父級 StatefulWidget 的State 對象
通過Context獲取
context
對象有一個(gè)findAncestorStateOfType()
方法宛逗,該方法可以從當(dāng)前節(jié)點(diǎn)沿著 widget 樹向上查找指定類型的 StatefulWidget 對應(yīng)的 State 對象坎匿。下面是實(shí)現(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ā)中便有了一個(gè)默認(rèn)的約定:如果 StatefulWidget 的狀態(tài)是希望暴露出的,應(yīng)當(dāng)在 StatefulWidget 中提供一個(gè)of
靜態(tài)方法來獲取其 State 對象快毛,開發(fā)者便可直接通過該方法來獲雀裥帷;如果 State不希望暴露唠帝,則不提供of
方法屯掖。這個(gè)約定在 Flutter SDK 里隨處可見。所以襟衰,上面示例中的Scaffold
也提供了一個(gè)of
方法贴铜,我們其實(shí)是可以直接調(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來獲取瀑晒! 步驟分兩步:
-
給目標(biāo)
StatefulWidget
添加GlobalKey
绍坝。//定義一個(gè)globalKey, 由于GlobalKey要保持全局唯一性,我們使用靜態(tài)變量存儲(chǔ) static GlobalKey<ScaffoldState> _globalKey= GlobalKey(); ... Scaffold( key: _globalKey , //設(shè)置key ... )
-
通過
GlobalKey
來獲取State
對象_globalKey.currentState.openDrawer()
GlobalKey 是 Flutter 提供的一種在整個(gè) App 中引用 element 的機(jī)制苔悦。如果一個(gè) 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)盡量避免使用它拗踢。另外,同一個(gè) GlobalKey 在整個(gè) widget 樹中必須是唯一的向臀,不能重復(fù)
1.7 通過 RenderObject 自定義 Widget
StatelessWidget
和 StatefulWidget
都是用于組合其它組件的秒拔,它們本身沒有對應(yīng)的 RenderObject。Flutter 組件庫中的很多基礎(chǔ)組件都不是通過StatelessWidget
和 StatefulWidget
來實(shí)現(xiàn)的飒硅,比如 Text 、Column作谚、Align等三娩,就好比搭積木,StatelessWidget
和 StatefulWidget
可以將積木搭成不同的樣子妹懒,但前提是得有積木雀监,而這些積木都是通過自定義 RenderObject 來實(shí)現(xiàn)的。實(shí)際上Flutter 最原始的定義組件的方式就是通過定義RenderObject 來實(shí)現(xiàn),而StatelessWidget
和 StatefulWidget
只是提供的兩個(gè)幫助類会前。我們簡單演示一下通過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() {
// 實(shí)現(xiàn)布局邏輯
}
@override
void paint(PaintingContext context, Offset offset) {
// 實(shí)現(xiàn)繪制
}
}
如果組件不會(huì)包含子組件好乐,則我們可以直接繼承自 LeafRenderObjectWidget ,它是 RenderObjectWidget 的子類瓦宜,而 RenderObjectWidget 繼承自 Widget 蔚万,我們可以看一下它的實(shí)現(xiàn):
abstract class LeafRenderObjectWidget extends RenderObjectWidget {
const LeafRenderObjectWidget({ Key? key }) : super(key: key);
@override
LeafRenderObjectElement createElement() => LeafRenderObjectElement(this);
}
很簡單,就是幫 widget 實(shí)現(xiàn)了createElement 方法临庇,它會(huì)為組件創(chuàng)建一個(gè) 類型為 LeafRenderObjectElement 的 Element對象反璃。如果自定義的 widget 可以包含子組件,則可以根據(jù)子組件的數(shù)量來選擇繼承SingleChildRenderObjectWidget 或 MultiChildRenderObjectWidget假夺,它們也實(shí)現(xiàn)了createElement() 方法淮蜈,返回不同類型的 Element 對象。