在Flutter中有Widget樹、Element樹與渲染樹(render tree),想要更彈性地自定元件,認識渲染樹就是必要的!

作為Flutter的直接使用者(end-user),想自定元件時,多半只需要繼承StatelessWidget、StatefulWidget,這兩個類別之目的,是讓直接使用的開發者在不用接觸Element或RenderObject的情況下,也能輕鬆自定元件。

這是因為StatelessWidget、StatefulWidget會建立的Element,都是ComponentElement的子類,它有一個無參數的build方法,ComponentElement的子類StatelessElement,其build方法僅回頭呼叫關聯的Widget的build,而StatefulElement的build只是呼叫關聯的State的build,兩者都傳入實例自身而成為BuildContext。

這也是定義StatelessWidget、StatefulWidget時,不會接觸Element的原因——都在StatelessWidget,或StatefulWidget的State之build中,組建Widget元件。

然而,框架無法(也不能)完全隱藏細節,開發者越能掌握框架提供的流程,越能知道框架是否適用,或能做出何種調整,使用Flutter時即如此,例如,就算只用StatelessWidget、StatefulWidget,也必須知道key特性設置,會影響Widget樹與Element樹的對應,從而影響最終畫面的顯示是否正確。

認識RenderObjectWidget

在Flutter中,除了Widget樹與Element樹之外,實際上,還有一棵渲染樹,它是由Element樹管理,由負責繪圖的RenderObject組成,想使用RenderObject來繪製元件,本身是個極為複雜的議題,不過,若想要更有彈性地設計元件,甚至用用Flutter來實作框架或元件庫,認識渲染樹就是必要的。

基本上,負責建立RenderObject的Widget是RenderObjectWidget,直接使用的開發者可能覺得陌生,其實,若追查Flutter中StatelessWidget、StatefulWidget的子類,會發現它們建構(build)的Widget子樹中,必然包含RenderObjectWidget節點,以Text為例,其build傳回的是RichText實例,也就是RenderObjectWidget的子類實例。

其實,有些用作排版的元件,例如Center,或者是一些繪圖元件,例如:RawImage、Opacity,也都是RenderObjectWidget,它們的createRenderObject實作會傳回各自實作的RenderObject實例。

從Widget到RenderObject

簡單來說,StatelessWidget、StatefulWidget會建立ComponentElement,而ComponentElement又會建立子Widget,這會持續到獲得RenderObjectWidget為止,這時就會建立RenderObject,開始組合渲染樹,只不過樹本身就有一定的複雜性。若我們想認識Widget到RenderObject的過程,可從指定底下的RichText給runApp開始:

RichText(
text: TextSpan(text: 'Hello, World'),
textDirection: TextDirection.ltr
)

RichText是RenderObjectWidget的子類,上面的RichText會指定為根Widget,稍微瞭解Flutter的開發者都知道,接下來會透過它的createElement建立對應的Element,並將Widget實例指定給這個Element,接著呼叫該Element的mount方法,在方法中會執行widget.createRenderObject(this)建立RenderObject,Element與RenderObject也有了關聯,也就是說,Element是Widget與RenderObject的橋樑。

於是,現在有了三棵樹了(雖然各只有一個根節點),如果在你的main流程中,又新建了一個RichText實例,並再度指定給runApp會如何呢?根據我先前專欄文章〈從Flutter看狀態管理〉,在沒有設置key特性的情況下,因為Widget的型態相同,就會用新的RichText實例來更新Element,也就是Element不會重建,只需參考至新的RichText實例。

Element接著會呼叫Widget的updateRenderObject方法,傳入自身(Element實作了BuildContext的行為)與既有的RenderObject,顧名思義,就是在updateRenderObject方法,更新RenderObject的特性,在RenderObject進行特性更新後,會呼叫自身的markNeedsPaint方法,表示需重繪,並在下個繪圖時程會呼叫paint,這時會看到畫面內容有變化。

在這種情況下,Widget樹雖然有了變化,然而Element樹只需要更新Widget,透過新的Widget組態來更新渲染樹,也就是不用重建Element樹與渲染樹,藉此獲得效能上的效益;如果想更詳細瞭解這個過程,可以參考〈How Flutter renders Widgets〉

實作RenderObjectWidget

想實作簡單且無子元件的RenderObjectWidget,可試著繼承子類LeafRenderObjectWidget,顧名思義,這會是只作為葉節點的RenderObjectWidget,我實作了一個小範例,純粹畫個方塊,不考慮排版、父子關係等,主要可用來觀察到createElement、createRenderObject、updateRenderObject等,了解何時被呼叫。

開發者何時需要自定RenderObjectWidget呢?一個可能的場合是想介入某元件繪圖前後之時,例如,在圖片上加上文字,或者令子元件呈現半透明等,Flutter提供的Opacity就是個範例,它是個RenderObjectWidget,其createRenderObject傳回的RenderObject,paint方法中會執行context.pushOpacity(offset, _alpha, super.paint),這會將子元件以指定的半透明值繪製,若要探討詳細的原始碼,可參考〈Flutter, what are Widgets, RenderObjects and Elements?〉

從Flutter排版元件不少是RenderObjectWidget來看,自定排版元件也是可能,但排版是極複雜議題,須先認識constraint、size在Flutter的意義,考慮父子元件關係,對此我們可參考官方文件〈Understanding constraints〉。

接著,我們可試著考量只有一個子元件的排版,也就是繼承SingleChildRenderObjectWidget,此時,能參考〈Flutter's Rendering Engine〉其中實作了個Stingy,子元件最大也只能是父元件指定constraints的minWidth、minHeight,且子元件總是固定在父元件的右下角。

在這些實作RenderObjectWidget的過程中,你會理解到Widget為何只是作為組態資訊,因為經常都只是將Widget包含的訊息,拿來更新RenderObject罷了;至於Element,主要是作為Widget與RenderObject的銜接,以及管理狀態之用。

Flutter的三棵樹

為了要能靈活使用Flutter,開發者必須要瞭解三棵樹?這麼看來,瀏覽器的DOM樹還可愛多了!其實,Flutter受到React的影響,Widget樹就相當於虛擬DOM,Element樹就像是DOM樹,至於渲染樹,瀏覽器中並沒有標準方法可以介入,然而,在Flutter中,可以介入渲染樹的渲染過程,因此,對於開發者而言,提供了更大的彈性。

這取決於開發者想基於Flutter達到的目的,直接使用的開發者多半只要知道Widget樹(以及key的作用);若想介入狀態管理,就要認識Element樹,必要時,甚至可直接繼承Widget、ComponentElement來組建Widget,不一定要繼承StatelessWidget、StatefulWidget;若是想介入渲染過程,如實作出Opacity之類的元件,認識渲染樹就會是必要課題!

專欄作者

熱門新聞

Advertisement