Flutter實(shí)戰(zhàn) Element與BuildContext

2021-03-09 14:42 更新

#14.2.1 Element

在“Widget 簡(jiǎn)介”一節(jié),我們介紹了 Widget 和 Element 的關(guān)系,我們知道最終的 UI 樹其實(shí)是由一個(gè)個(gè)獨(dú)立的 Element 節(jié)點(diǎn)構(gòu)成。我們也說(shuō)過(guò)組件最終的 Layout、渲染都是通過(guò)RenderObject來(lái)完成的,從創(chuàng)建到渲染的大體流程是:根據(jù) Widget 生成 Element,然后創(chuàng)建相應(yīng)的RenderObject并關(guān)聯(lián)到Element.renderObject屬性上,最后再通過(guò)RenderObject來(lái)完成布局排列和繪制。

Element 就是 Widget 在 UI 樹具體位置的一個(gè)實(shí)例化對(duì)象,大多數(shù) Element 只有唯一的renderObject,但還有一些 Element 會(huì)有多個(gè)子節(jié)點(diǎn),如繼承自RenderObjectElement的一些類,比如MultiChildRenderObjectElement。最終所有 Element 的 RenderObject 構(gòu)成一棵樹,我們稱之為”Render Tree“即”渲染樹“??偨Y(jié)一下,我們可以認(rèn)為 Flutter的UI 系統(tǒng)包含三棵樹:Widget 樹、Element 樹、渲染樹。他們的依賴關(guān)系是:Element 樹根據(jù) Widget 樹生成,而渲染樹又依賴于Element 樹,如圖14-0所示。

圖14-0

現(xiàn)在我們重點(diǎn)看一下 Element,Element 的生命周期如下:

  1. Framework 調(diào)用Widget.createElement 創(chuàng)建一個(gè) Element 實(shí)例,記為element
  2. Framework 調(diào)用 element.mount(parentElement,newSlot) ,mount 方法中首先調(diào)用element所對(duì)應(yīng) Widget 的createRenderObject方法創(chuàng)建與element相關(guān)聯(lián)的 RenderObject 對(duì)象,然后調(diào)用element.attachRenderObject方法將element.renderObject添加到渲染樹中插槽指定的位置(這一步不是必須的,一般發(fā)生在 Element 樹結(jié)構(gòu)發(fā)生變化時(shí)才需要重新attach)。插入到渲染樹后的element就處于“active”狀態(tài),處于“active”狀態(tài)后就可以顯示在屏幕上了(可以隱藏)。
  3. 當(dāng)有父 Widget 的配置數(shù)據(jù)改變時(shí),同時(shí)其State.build返回的 Widget 結(jié)構(gòu)與之前不同,此時(shí)就需要重新構(gòu)建對(duì)應(yīng)的 Element 樹。為了進(jìn)行 Element 復(fù)用,在 Element 重新構(gòu)建前會(huì)先嘗試是否可以復(fù)用舊樹上相同位置的 element,element 節(jié)點(diǎn)在更新前都會(huì)調(diào)用其對(duì)應(yīng) Widget 的canUpdate方法,如果返回true,則復(fù)用舊 Element,舊的 Element 會(huì)使用新 Widget 配置數(shù)據(jù)更新,反之則會(huì)創(chuàng)建一個(gè)新的 Element。Widget.canUpdate主要是判斷newWidgetoldWidgetruntimeTypekey是否同時(shí)相等,如果同時(shí)相等就返回true,否則就會(huì)返回false。根據(jù)這個(gè)原理,當(dāng)我們需要強(qiáng)制更新一個(gè) Widget 時(shí),可以通過(guò)指定不同的 Key 來(lái)避免復(fù)用。
  4. 當(dāng)有祖先 Element 決定要移除element 時(shí)(如 Widget 樹結(jié)構(gòu)發(fā)生了變化,導(dǎo)致element對(duì)應(yīng)的 Widget 被移除),這時(shí)該祖先 Element 就會(huì)調(diào)用deactivateChild 方法來(lái)移除它,移除后element.renderObject也會(huì)被從渲染樹中移除,然 后 Framework 會(huì)調(diào)用element.deactivate 方法,這時(shí)element狀態(tài)變?yōu)椤癷nactive”狀態(tài)。
  5. “inactive”態(tài)的 element 將不會(huì)再顯示到屏幕。為了避免在一次動(dòng)畫執(zhí)行過(guò)程中反復(fù)創(chuàng)建、移除某個(gè)特定 element,“inactive”態(tài)的 element 在當(dāng)前動(dòng)畫最后一幀結(jié)束前都會(huì)保留,如果在動(dòng)畫執(zhí)行結(jié)束后它還未能重新變成“active”狀態(tài),F(xiàn)ramework 就會(huì)調(diào)用其unmount方法將其徹底移除,這時(shí) element 的狀態(tài)為defunct,它將永遠(yuǎn)不會(huì)再被插入到樹中。
  6. 如果element要重新插入到 Element 樹的其它位置,如elementelement的祖先擁有一個(gè)GlobalKey(用于全局復(fù)用元素),那么 Framework 會(huì)先將 element 從現(xiàn)有位置移除,然后再調(diào)用其activate方法,并將其renderObject重新 attach 到渲染樹。

看完 Element 的生命周期,可能有些讀者會(huì)有疑問(wèn),開發(fā)者會(huì)直接操作 Element 樹嗎?其實(shí)對(duì)于開發(fā)者來(lái)說(shuō),大多數(shù)情況下只需要關(guān)注 Widget 樹就行,F(xiàn)lutter 框架已經(jīng)將對(duì) Widget 樹的操作映射到了Element樹上,這可以極大的降低復(fù)雜度,提高開發(fā)效率。但是了解 Element 對(duì)理解整個(gè) Flutter UI 框架是至關(guān)重要的,F(xiàn)lutter 正是通過(guò) Element 這個(gè)紐帶將 Widget 和 RenderObject 關(guān)聯(lián)起來(lái),了解 Element 層不僅會(huì)幫助讀者對(duì) Flutter UI 框架有個(gè)清晰的認(rèn)識(shí),而且也會(huì)提高自己的抽象能力和設(shè)計(jì)能力。另外在有些時(shí)候,我們必須得直接使用 Element 對(duì)象來(lái)完成一些操作,比如獲取主題 Theme 數(shù)據(jù),具體細(xì)節(jié)將在下文介紹。

#14.2.2 BuildContext

我們已經(jīng)知道,StatelessWidgetStatefulWidgetbuild方法都會(huì)傳一個(gè)BuildContext對(duì)象:

Widget build(BuildContext context) {}

我們也知道,在很多時(shí)候我們都需要使用這個(gè)context 做一些事,比如:

Theme.of(context) //獲取主題
Navigator.push(context, route) //入棧新路由
Localizations.of(context, type) //獲取Local
context.size //獲取上下文大小
context.findRenderObject() //查找當(dāng)前或最近的一個(gè)祖先RenderObject

那么BuildContext到底是什么呢,查看其定義,發(fā)現(xiàn)其是一個(gè)抽象接口類:

abstract class BuildContext {
    ...
}

那這個(gè)context對(duì)象對(duì)應(yīng)的實(shí)現(xiàn)類到底是誰(shuí)呢?我們順藤摸瓜,發(fā)現(xiàn)build調(diào)用是發(fā)生在StatelessWidgetStatefulWidget對(duì)應(yīng)的StatelessElementStatefulElementbuild方法中,以StatelessElement為例:

class StatelessElement extends ComponentElement {
  ...
  @override
  Widget build() => widget.build(this);
  ...
}

發(fā)現(xiàn)build傳遞的參數(shù)是this,很明顯!這個(gè)BuildContext就是StatelessElement。同樣,我們同樣發(fā)現(xiàn)StatefulWidgetcontextStatefulElement。但StatelessElementStatefulElement本身并沒(méi)有實(shí)現(xiàn)BuildContext接口,繼續(xù)跟蹤代碼,發(fā)現(xiàn)它們間接繼承自Element類,然后查看Element類定義,發(fā)現(xiàn)Element類果然實(shí)現(xiàn)了BuildContext接口:

class Element extends DiagnosticableTree implements BuildContext {
    ...
}

至此真相大白,BuildContext就是 widget 對(duì)應(yīng)的Element,所以我們可以通過(guò)contextStatelessWidgetStatefulWidgetbuild方法中直接訪問(wèn)Element對(duì)象。我們獲取主題數(shù)據(jù)的代碼Theme.of(context)內(nèi)部正是調(diào)用了Element的dependOnInheritedWidgetOfExactType()方法。

思考題:為什么 build 方法的參數(shù)不定義成 Element 對(duì)象,而要定義成 BuildContext ?

#進(jìn)階

我們可以看到 Element 是 Flutter UI 框架內(nèi)部連接 widget 和RenderObject的紐帶,大多數(shù)時(shí)候開發(fā)者只需要關(guān)注 widget 層即可,但是 widget 層有時(shí)候并不能完全屏蔽Element細(xì)節(jié),所以 Framework 在StatelessWidgetStatefulWidget中通過(guò)build方法參數(shù)又將Element對(duì)象也傳遞給了開發(fā)者,這樣一來(lái),開發(fā)者便可以在需要時(shí)直接操作Element對(duì)象。那么現(xiàn)在筆者提兩個(gè)問(wèn)題,請(qǐng)讀者先自己思考一下:

  1. 如果沒(méi)有 widget 層,單靠Element層是否可以搭建起一個(gè)可用的 UI 框架?如果可以應(yīng)該是什么樣子?
  2. Flutter UI 框架能不做成響應(yīng)式嗎?

對(duì)于問(wèn)題1,答案當(dāng)然是肯定的,因?yàn)槲覀冎罢f(shuō)過(guò) widget 樹只是Element樹的映射,我們完全可以直接通過(guò) Element 來(lái)搭建一個(gè) UI 框架。下面舉一個(gè)例子:

我們通過(guò)純粹的 Element 來(lái)模擬一個(gè)StatefulWidget的功能,假設(shè)有一個(gè)頁(yè)面,該頁(yè)面有一個(gè)按鈕,按鈕的文本是一個(gè)9位數(shù),點(diǎn)擊一次按鈕,則對(duì)9個(gè)數(shù)隨機(jī)排一次序,代碼如下:

class HomeView extends ComponentElement{
  HomeView(Widget widget) : super(widget);
  String text = "123456789";


  @override
  Widget build() {
    Color primary=Theme.of(this).primaryColor; //1
    return GestureDetector(
      child: Center(
        child: FlatButton(
          child: Text(text, style: TextStyle(color: primary),),
          onPressed: () {
            var t = text.split("")..shuffle();
            text = t.join();
            markNeedsBuild(); //點(diǎn)擊后將該Element標(biāo)記為dirty,Element將會(huì)rebuild
          },
        ),
      ),
    );
  }
}

  • 上面build方法不接收參數(shù),這一點(diǎn)和在StatelessWidgetStatefulWidgetbuild(BuildContext)方法不同。代碼中需要用到BuildContext的地方直接用this代替即可,如代碼注釋1處Theme.of(this)參數(shù)直接傳this即可,因?yàn)楫?dāng)前對(duì)象本身就是Element實(shí)例。
  • 當(dāng)text發(fā)生改變時(shí),我們調(diào)用markNeedsBuild()方法將當(dāng)前 Element 標(biāo)記為 dirty 即可,標(biāo)記為 dirty 的 Element 會(huì)在下一幀中重建。實(shí)際上,State.setState()在內(nèi)部也是調(diào)用的markNeedsBuild()方法。
  • 上面代碼中 build 方法返回的仍然是一個(gè) widget,這是由于 Flutter 框架中已經(jīng)有了 widget 這一層,并且組件庫(kù)都已經(jīng)是以 widget 的形式提供了,如果在 Flutter 框架中所有組件都像示例的HomeView一樣以Element形式提供,那么就可以用純Element來(lái)構(gòu)建UI了HomeView的 build 方法返回值類型就可以是Element了。

如果我們需要將上面代碼在現(xiàn)有 Flutter 框架中跑起來(lái),那么還是得提供一個(gè)“適配器”widget 將HomeView結(jié)合到現(xiàn)有框架中,下面CustomHome就相當(dāng)于“適配器”:

class CustomHome extends Widget {
  @override
  Element createElement() {
    return HomeView(this);
  }
}

現(xiàn)在就可以將CustomHome添加到 widget 樹了,我們?cè)谝粋€(gè)新路由頁(yè)創(chuàng)建它,最終效果如下如圖14-1和14-2(點(diǎn)擊后)所示:

圖14-1 圖14-2

點(diǎn)擊按鈕則按鈕文本會(huì)隨機(jī)排序。

對(duì)于問(wèn)題2,答案當(dāng)然也是肯定的,F(xiàn)lutter engine 提供的 dart API 是原始且獨(dú)立的,這個(gè)與操作系統(tǒng)提供的 API 類似,上層 UI 框架設(shè)計(jì)成什么樣完全取決于設(shè)計(jì)者,完全可以將 UI 框架設(shè)計(jì)成 Android 風(fēng)格或 iOS 風(fēng)格,但這些事 Google 不會(huì)再去做,我們也沒(méi)必要再去搞這一套,這是因?yàn)轫憫?yīng)式的思想本身是很棒的,之所以提出這個(gè)問(wèn)題,是因?yàn)楣P者認(rèn)為做與不做是一回事,但知道能不能做是另一回事,這能反映出我們對(duì)知識(shí)的理解程度。

#總結(jié)

本節(jié)詳細(xì)的介紹了Element的生命周期,以及它 Widget、BuildContext 的關(guān)系,也介紹了 Element 在 Flutter UI 系統(tǒng)中的角色和作用,我們將在下一節(jié)介紹 Flutter UI 系統(tǒng)中另一個(gè)重要的角色 RenderObject。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)