本文是『 深入淺出 Flutter Framework 』系列文章的第一篇,主要以不同型別 Widget 的核心方法為切入點,對其展開詳細分析。
本文同時發表於我的個人部落格
Overview
Flutter 作為一種新興跨平臺解決方案,自 2017 年 Google 在 I/O 大會上推出後,尤其是在 2018 年 I/O 大會上釋出第一個預覽版後,迅速引起移動開發者的廣泛關注,併成為時下最熱門的跨平臺解決方案 ( 沒有之一 ) !
本系列文章將深入 Flutter Framework 內部逐步去分析其核心概念和流程,主要包括:
- 『 深入淺出 Flutter Framework 之 Widget 』
- 『 深入淺出 Flutter Framework 之 BuildOwner 』
- 『 深入淺出 Flutter Framework 之 Element 』
- 『 深入淺出 Flutter Framework 之 PipelineOwner 』
- 『 深入淺出 Flutter Framework 之 RenderObejct 』
- 『 深入淺出 Flutter Framework 之 Layer 』
- 『 深入淺出 Flutter Framework 之 Binding 』
- 『 深入淺出 Flutter Framework 之 Rendering Pipeline 』
- 『 深入淺出 Flutter Framework 之 自定義 Widget 』
其中,前 7 篇屬於基礎篇,分別介紹 Flutter 中幾個最核心的概念。Rendering Pipeline 篇則是在此基礎上從 Build、Layout 到 Paint 的流程將它們串起來,分析 Flutter UI 是如何建立的、如何更新的。最後,自定義 Widget 屬於回顧、實踐篇,分析自定義一個 Render Widget 至少需要哪些步驟。
如下圖所示,Flutter 整體分為三層:Framework (dart)、Engine (C/C++)、Embedder (Platform),上述文章主要集中在 Framework 這一層。
Widget
Everything’s a widget.
在開發 Flutter 應用過程中,接觸最多的無疑就是Widget
,是『描述』 Flutter UI 的基本單元,通過Widget
可以做到:
- 描述 UI 的層級結構 (通過
Widget
巢狀); - 定製 UI 的具體樣式 (如:
font
、color
等); - 指導 UI 的佈局過程 (如:
padding
、center
等); - ...
Google 在設計Widget
時,還賦予它一些鮮明的特點:
-
宣告式 UI —— 相對於傳統 Native 開發中的命令式 UI,宣告式 UI 有不少優勢,如:開發效率顯著提升、UI 可維護性明顯加強等;
-
不可變性 —— Flutter 中所有
Widget
都是不可變的(immutable),即其內部成員都是不可變的(final
),對於變化的部分需要通過「Stateful Widget-State」的方式實現; -
組合大於繼承 ——
Widget
設計遵循組合大於繼承這一優秀的設計理念,通過將多個功能相對單一的Widget
組合起來便可得到功能相對複雜的Widget
。
在Widget
類定義處有這樣一段註釋:
Widget
的本質:用於配置Element
的,Widget
本質上是 UI 的配置資訊 (附帶部分業務邏輯)。
我們通常會將通過
Widget
描述的 UI 層級結構稱之為「Widget Tree」,但與「Element Tree」、「RenderObject Tree」以及「Layer Tree」相比,實質上並不存在「Widget Tree」。為了描述方便,將 Widget 組合描述的 UI 層級結構稱之為「Widget Tree」,也未嘗不可。
分類
如上圖所示,按照功能劃分Widget
大致可以分為 3 類:
-
「Component Widget」 —— 組合類 Widget,這類 Widget 都直接或間接繼承於
StatelessWidget
或StatefulWidget
,上一小節提到過在 Widget 設計上遵循組合大於繼承的原則,通過組合功能相對單一的 Widget 可以得到功能更為複雜的 Widget。平常的業務開發主要是在開發這一型別的 Widget; -
「Proxy Widget」 —— 代理類 Widget,正如其名,「Proxy Widget」本身並不涉及 Widget 內部邏輯,只是為「Child Widget」提供一些附加的中間功能。典型的如:
InheritedWidget
用於在「Descendant Widgets」間傳遞共享資訊、ParentDataWidget
用於配置「Descendant Renderer Widget」的佈局資訊; -
「Renderer Widget」 —— 渲染類 Widget,是最核心的
Widget
型別,會直接參與後面的「Layout」、「Paint」流程,無論是「Component Widget」還是「Proxy Widget」最終都會對映到「Renderer Widget」上,否則將無法被繪製到螢幕上。這 3 類 Widget 中,只有「Renderer Widget」有與之一一對應的「Render Object」。
核心方法原始碼分析
下面,我們重點介紹各型別 Widget 的核心方法,以便更好地理解 Widget 是如何參與整個 UI 的構建過程。
Widget
Widget
,所有 Widget 的基類。
如上圖所示,在 Widget
基類中有 3 個重要的方法 (屬性):
- Key key —— 在同一父節點下,用作兄弟節點間的唯一標識,主要用於控制當 Widget 更新時,對應的 Element 如何處理 (是更新還是新建)。若某 Widget 是其「Parent Widget」唯一的子節點時,一般不用設定 key;
GlobalKey 是一類較特殊的 key,在介紹 Element 時會附帶介紹。
-
Element createElement() —— 每個
Widget
都有一個與之對應的Element
,由該方法負責建立,createElement
可以理解為設計模式中的工廠方法,具體的Element
型別由對應的Widget
子類負責建立; -
static bool canUpdate(Widget oldWidget, Widget newWidget) —— 是否可以用 new widget 修改前一幀用 old widget 生成的 Element,而不是建立新的 Element,
Widget
類的預設實現為:2個Widget
的runtimeType
與key
都相等時,返回true
,即可以直接更新 (key 為 null 時,認為相等)。
上述更新流程,同樣在介紹 Element 時會重點分析。
StatelessWidget
無狀態-組合型 Widget,由其build
方法描述組合 UI 的層級結構。在其生命週期內狀態不可變。
ps: 對於有父子關係的類,在子類中只會介紹新增或有變化的方法
-
StatelessElement createElement() ——「Stateless Widget」對應的 Element 為
StatelessElement
,一般情況下StatelessWidget
子類不必重寫該方法,即子類對應的 Element 也是StatelessElement
; -
Widget build(BuildContext context) —— 算是 Flutter 體系中的核心方法之一,以『宣告式 UI』的形式描述了該組合式 Widget 的 UI 層級結構及樣式資訊,也是開發 Flutter 應用的主要工作『場所』。該方法在 3 種情況下被呼叫:
- Widget 第一次被加入到 Widget Tree 中 (更準確地說是其對應的 Element 被加入到 Element Tree 時,即 Element 被掛載『mount』時);
- 「Parent Widget」修改了其配置資訊;
- 該 Widget 依賴的「Inherited Widget」發生變化時。
當「Parent Widget」或 依賴的「Inherited Widget」頻繁變化時,build
方法也會頻繁被呼叫。因此,提升build
方法的效能就顯得十分重要,Flutter 官方給出了幾點建議:
-
減少不必要的中間節點,即減少 UI 的層級,如:對於「Single Child Widget」,沒必要通過組合「Row」、「Column」、「Padding」、「SizedBox」等複雜的 Widget 達到某種佈局的目標,或許通過簡單的「Align」、「CustomSingleChildLayout」即可實現。又或者,為了實現某種複雜精細的 UI 效果,不一定要通過組合多個「Container」,再附加「Decoration」來實現,通過 「CustomPaint」自定義或許是更好的選擇;
-
儘可能使用
const
Widget,為 Widget 提供const
構造方法;關於 const constructor 推薦 Dart Constant Constructors 看看這篇文章的評論。
-
必要時,可以將「Stateless Widget」重構成「Stateful Widget」,以便可以使用「Stateful Widget」中一些特定的優化手法,如:快取「sub trees」的公共部分,並在改變樹結構時使用
GlobalKey
; -
儘量減小 rebuilt 範圍,如:某個 Widget 因使用了「Inherited Widget」,導致頻繁 rebuilt,可以將真正依賴「Inherited Widget」的部分提取出來,封裝成更小的獨立 Widget,並儘量將該獨立 Widget 推向樹的葉子節點,以便減小 rebuilt 時受影響的範圍。
StatefulWidget
有狀態-組合型 Widget,但要注意的是StatefulWidget
本身還是不可變的,其可變狀態存在於State
中。
-
StatefulElement createElement() ——「Stateful Widget」對應的 Element 為
StatefulElement
,一般情況下StatefulWidget
子類不用重寫該方法,即子類對應的Element 也是StatefulElement
; -
State createState() —— 建立對應的 State,該方法在
StatefulElement
的構造方法中被呼叫。可以簡單地理解為當「Stateful Widget」被新增到 Widget Tree 時會呼叫該方法。
// 程式碼已精簡處理(本文中其他程式碼會做同樣的簡化處理)
StatefulElement(StatefulWidget widget)
: _state = widget.createState(), super(widget) {
_state._element = this;
_state._widget = widget;
}
複製程式碼
實際上是「Stateful Widget」對應的「Stateful Element」被新增到 Element Tree 時,伴隨「Stateful Element」的初始化,
createState
方法被呼叫。從後文可知一個 Widget 例項可以對應多個 Element 例項 (也就是同一份配置資訊 (Widget) 可以在 Element Tree 上不同位置配置多個 Element 節點),因此,createState
方法在「Stateful Widget」生命週期內可能會被呼叫多次。
另外,需要注意的是配有
GlobalKey
的 Widget 對應的 Element 在整個 Element Tree 中只有一個例項。
State
The logic and internal state for a 「Stateful Widget」.
State 用於處理「Stateful Widget」的業務邏輯以及可變狀態。 由於其內部狀態是可變的,故 State 有較複雜的生命週期:
如上圖,State 的生命週期大致可以分為 8 個階段:- 在對應的「Stateful Element」被掛載 (mount) 到樹上時,通過
StatefulElement.constructor
-->StatefulWidget.createState
建立 State 例項;
從
StatefulElement.constructor
中的_state._element = this;
可知,State._emelent
指向了對應的 Element 例項,而我們熟知的State.context
引用的就是這個_element
:BuildContext get context => _element;
。State
例項與Element
例項間的繫結關係一經確定,在整個生命週期內不會再變了 (Element 對應的 Widget 可能會變,但對應的 State 永遠不會變),期間,Element
可以在樹上移動,但上述關係不會變 (即「Stateful Element」是帶著狀態移動的)。
-
StatefulElement 在掛載過程中接著會呼叫
State.initState
,子類可以重寫該方法執行相關的初始化操作 (此時可以引用context
、widget
屬性); -
同樣在掛載過程中會呼叫
State.didChangeDependencies
,該方法在 State 依賴的物件 (如:「Inherited Widget」) 狀態發生變化時也會被呼叫,*子類很少需要重寫該方法,*除非有非常耗時不宜在build
中進行的操作,因為在依賴有變化時build
方法也會被呼叫; -
此時,State 初始化已完成,其
build
方法此後可能會被多次呼叫,在狀態變化時 State 可通過setState
方法來觸發其子樹的重建; -
此時,「element tree」、「renderobject tree」、「layer tree」已構建完成,完整的 UI 應該已呈現出來。此後因為變化,「element tree」中「parent element」可能會對樹上該位置的節點用新配置 (Widget) 進行重建,當新老配置 (oldWidget、newWidget)具有相同的「runtimeType」&&「key」時,framework 會用 newWidget 替換 oldWidget,並觸發一系列的更新操作 (在子樹上遞迴進行)。同時,
State.didUpdateWidget
方法被呼叫,子類重寫該方法去響應 Widget 的變化;
上述 3 棵樹以及更新流程在後續文章中會有詳細介紹
- 在 UI 更新過程中,任何節點都有被移除的可能,State 也會隨之移除,(如上一步中「runtimeType」||「key」不相等時)。此時會呼叫
State.deactivate
方法,由於被移除的節點可能會被重新插入樹中某個新的位置上,故子類重寫該方法以清理與節點位置相關的資訊 (如:該 State 對其他 element 的引用)、同時,不應在該方法中做資源清理;
重新插入操作必須在當前幀動畫結束之前
-
當節點被重新插入樹中時,
State.build
方法被再次呼叫; -
對於在當前幀動畫結束時尚未被重新插入的節點,
State.dispose
方法被執行,State 生命週期隨之結束,此後再呼叫State.setState
方法將報錯。子類重寫該方法以釋放任何佔用的資源。
setState
方法:
void setState(VoidCallback fn) {
assert(fn != null);
assert(() {
if (_debugLifecycleState == _StateLifecycle.defunct) {
throw FlutterError.fromParts(<DiagnosticsNode>[...]);
}
if (_debugLifecycleState == _StateLifecycle.created && !mounted) {
throw FlutterError.fromParts(<DiagnosticsNode>[...]);
}
return true;
}());
final dynamic result = fn() as dynamic;
assert(() {
if (result is Future) {
throw FlutterError.fromParts(<DiagnosticsNode>[...]);
}
return true;
}());
_element.markNeedsBuild();
}
複製程式碼
從上述原始碼可以看到,關於setState
方法有幾點值得關注:
-
在
State.dispose
後不能呼叫setState
; -
在 State 的構造方法中不能呼叫
setState
; -
setState
方法的回撥函式 (fn
) 不能是非同步的 (返回值為Future
),原因很簡單,因為從流程設計上 framework 需要根據回撥函式產生的新狀態去重新整理 UI; -
通過
setState
方法之所以能更新 UI,是在其內部呼叫_element.markNeedsBuild()
實現的 (具體過程在介紹 Element 時再詳細分析)。
關於 State 最後再強調 2 點:
-
若
State.build
方法依賴了自身狀態會變化的物件,如:ChangeNotifier
、Stream
或其他可以被訂閱的物件,需要確保在initState
、didUpdateWidget
、dispose
等 3 方法間有正確的訂閱 (subscribe) 與取消訂閱 (unsubscribe) 的操作:- 在
initState
中執行 subscribe; - 如果關聯的「Stateful Widget」與訂閱有關,在
didUpdateWidget
中先取消舊的訂閱,再執行新的訂閱; - 在
dispose
中執行 unsubscribe。
- 在
-
在
State.initState
方法中不能呼叫BuildContext.dependOnInheritedWidgetOfExactType
,但State.didChangeDependencies
會隨之執行,在該方法中可以呼叫。
ParentDataWidget
ParentDataWidget
以及下面要介紹的InheritedElement
都繼承自ProxyWidget
,由於ProxyWidget
作為抽象基類本身沒有任何功能,故下面直接介紹ParentDataWidget
、InheritedElement
。
ParentDataWidget
作為 Proxy 型 Widget,其功能主要是為其他 Widget 提供ParentData
資訊。雖然其 child widget 不一定是 RenderObejctWidget 型別,但其提供的ParentData
資訊最終都會落地到 RenderObejctWidget 型別子孫 Widget 上。
ParentData 是『parent renderobject』在 layout『child renderobject』時使用的輔助定位資訊,詳細資訊會在介紹 RenderObject 時介紹。
void attachRenderObject(dynamic newSlot) {
assert(_ancestorRenderObjectElement == null);
_slot = newSlot;
_ancestorRenderObjectElement = _findAncestorRenderObjectElement();
_ancestorRenderObjectElement?.insertChildRenderObject(renderObject, newSlot);
final ParentDataElement<RenderObjectWidget> parentDataElement = _findAncestorParentDataElement();
if (parentDataElement != null)
_updateParentData(parentDataElement.widget);
}
ParentDataElement<RenderObjectWidget> _findAncestorParentDataElement() {
Element ancestor = _parent;
while (ancestor != null && ancestor is! RenderObjectElement) {
if (ancestor is ParentDataElement<RenderObjectWidget>)
return ancestor;
ancestor = ancestor._parent;
}
return null;
}
void _updateParentData(ParentDataWidget<RenderObjectWidget> parentData) {
parentData.applyParentData(renderObject);
}
複製程式碼
上面這段程式碼來自RenderObjectElement
,可以看到在其attachRenderObject
方法第 6 行從祖先節點找ParentDataElement
,如果找到就用其 Widget(ParentDataWidget) 中的 parentData 資訊去設定 Render Obejct。在查詢過程中如查到RenderObjectElement
(第 13 行),說明當前 RenderObject 沒有 Parent Data 資訊。
最終會呼叫到ParentDataWidget.applyParentData(RenderObject renderObject)
,子類需要重寫該方法,以便設定對應RenderObject.parentData
。
來看個例子,通常配合Stack
使用的Positioned
(繼承自ParentDataWidget):
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is StackParentData);
final StackParentData parentData = renderObject.parentData;
bool needsLayout = false;
if (parentData.left != left) {
parentData.left = left;
needsLayout = true;
}
...
if (parentData.width != width) {
parentData.width = width;
needsLayout = true;
}
...
if (needsLayout) {
final AbstractNode targetParent = renderObject.parent;
if (targetParent is RenderObject)
targetParent.markNeedsLayout();
}
}
複製程式碼
可以看到,Positioned
在必要時將自己的屬性賦值給了對應的RenderObject.parentData
(此處是StackParentData
),並對「parent render object」呼叫markNeedsLayout
(第 19 行),以便重新 layout,畢竟修改了佈局相關的資訊。
abstract class ParentDataWidget<T extends RenderObjectWidget> extends ProxyWidget
複製程式碼
如上所示,ParentDataWidget
在定義上使用了泛型<T extends RenderObjectWidget>
,其背後的含義是:
從當前ParentDataWidget
節點向上追溯形成的祖先節點鏈(『parent widget chain』)上,在 2 個ParentDataWidget
型別的節點形成的鏈上至少要有一個『RenderObject Widget』型別的節點。因為一個『RenderObject Widget』不能接受來自 2 個及以上『ParentData Widget』的資訊。
InheritedWidget
InheritedWidget 用於在樹上向下傳遞資料。 通過BuildContext.dependOnInheritedWidgetOfExactType
可以獲取最近的「Inherited Widget」,需要注意的是通過這種方式獲取「Inherited Widget」時,當「Inherited Widget」狀態有變化時,會導致該引用方 rebuild。
具體原理在介紹 Element 時會詳細分析。
通常,為了使用方便會「Inherited Widget」會提供靜態方法of
,在該方法中呼叫BuildContext.dependOnInheritedWidgetOfExactType
。of
方法可以直接返回「Inherited Widget」,也可以是具體的資料。
有時,「Inherited Widget」是作為另一個類的實現細節而存在的,其本身是私有的(外部不可見),此時of
方法就會放到對外公開的類上。最典型的例子就是Theme
,其本身是StatelessWidget
型別,但其內部建立了一個「Inherited Widget」:_InheritedTheme
,of
方法就定義在上Theme
上:
static ThemeData of(BuildContext context, { bool shadowThemeOnly = false }) {
final _InheritedTheme inheritedTheme = context.dependOnInheritedWidgetOfExactType<_InheritedTheme>();
return ThemeData.localize(theme, theme.typography.geometryThemeFor(category));
}
複製程式碼
該of
方法返回的是ThemeData
型別的具體資料,並在其內部首先呼叫了BuildContext.dependOnInheritedWidgetOfExactType
。
我們經常使用的「Inherited Widget」莫過於MediaQuery
,同樣提供了of
方法:
static MediaQueryData of(BuildContext context, { bool nullOk = false }) {
final MediaQuery query = context.dependOnInheritedWidgetOfExactType<MediaQuery>();
if (query != null)
return query.data;
if (nullOk)
return null;
}
複製程式碼
-
InheritedElement createElement() ——「Inherited Widget」對應的 Element 為
InheritedElement
,一般情況下InheritedElement
子類不用重寫該方法; -
bool updateShouldNotify(covariant InheritedWidget oldWidget) —— 在「Inherited Widget」rebuilt 時判斷是否需要 rebuilt 那些依賴它的 Widget;
如下是MediaQuery.updateShouldNotify
的實現,在新老Widget.data
不相等時才 rebuilt 那依賴的 Widget。
bool updateShouldNotify(MediaQuery oldWidget) => data != oldWidget.data;
複製程式碼
RenderObjectWidget
真正與渲染相關的 Widget,屬於最核心的型別,一切其他型別的 Widget 要渲染到螢幕上,最終都要回歸到該型別的 Widget 上。
-
RenderObjectElement createElement() ——「RenderObject Widget」對應的 Element 為
RenderObjectElement
,由於RenderObjectElement
也是抽象類,故子類需要重寫該方法; -
RenderObject createRenderObject(BuildContext context) —— 核心方法,建立 Render Widget 對應的 Render Object,同樣子類需要重寫該方法。該方法在對應的 Element 被掛載到樹上時呼叫(
Element.mount
),即在 Element 掛載過程中同步構建了「Render Tree」(詳細過程後續文章會詳細分析);
@override
RenderFlex createRenderObject(BuildContext context) {
return RenderFlex(
direction: direction,
mainAxisAlignment: mainAxisAlignment,
mainAxisSize: mainAxisSize,
crossAxisAlignment: crossAxisAlignment,
textDirection: getEffectiveTextDirection(context),
verticalDirection: verticalDirection,
textBaseline: textBaseline,
);
}
複製程式碼
上面是Flex.createRenderObject
的原始碼,真實感受一下 (還是程式碼更有感覺)。可以看到,用Flex
的資訊(配置)初始化了RenderFlex
。
Flex
是Row
、Column
的基類,RenderFlex
繼承自RenderBox
,後者繼續自RenderObject
。
- void updateRenderObject(BuildContext context, covariant RenderObject renderObject) —— 核心方法,在 Widget 更新後,修改對應的 Render Object。該方法在首次 build 以及需要更新 Widget 時都會呼叫;
@override
void updateRenderObject(BuildContext context, covariant RenderFlex renderObject) {
renderObject
..direction = direction
..mainAxisAlignment = mainAxisAlignment
..mainAxisSize = mainAxisSize
..crossAxisAlignment = crossAxisAlignment
..textDirection = getEffectiveTextDirection(context)
..verticalDirection = verticalDirection
..textBaseline = textBaseline;
}
複製程式碼
Flex.updateRenderObject
的原始碼也很簡單,與Flex.createRenderObject
幾乎一一對應,用當前Flex
的資訊修改renderObject
。
- void didUnmountRenderObject(covariant RenderObject renderObject) —— 對應的「Render Object」從「Render Tree」上移除時呼叫該方法。
RenderObjectWidget
的幾個子類:LeafRenderObjectWidget
、SingleChildRenderObjectWidget
、MultiChildRenderObjectWidget
只是重寫了createElement
方法以便返回各自對應的具體的 Element 類例項。
小結
至此,重要的基礎型 Widget 基本介紹完了,總結一下:
-
Widget 本質上是 UI 的配置資訊 (附加部分業務邏輯),並不存在一顆真實的「Widget Tree」(與「Element Tree」、「RenderObject Tree」以及「Layer Tree」相比);
-
Widget 從功能上可以分為 3 類:「Component Widget」、「Proxy Widget」以及「Renderer Widget」;
-
Widget 與 Element 一一對應,Widget 提供建立 Element 的方法 (
createElement
,本質上是一個工廠方法); -
只有「Renderer Widget」才會參與最終的 UI 生成過程(Layout、Paint),只有該型別的 Widget 才有與之對應的「Render Object」,同樣由其提供建立方法(
createRenderObject
)。
下篇再見!