本文是『 深刻淺出 Flutter Framework 』系列文章的第一篇,主要以不一樣類型 Widget 的核心方法爲切入點,對其展開詳細分析。html
本文同時發表於個人我的博客git
Flutter 做爲一種新興跨平臺解決方案,自 2017 年 Google 在 I/O 大會上推出後,尤爲是在 2018 年 I/O 大會上發佈第一個預覽版後,迅速引發移動開發者的普遍關注,併成爲時下最熱門的跨平臺解決方案 ( 沒有之一 ) !github
本系列文章將深刻 Flutter Framework 內部逐步去分析其核心概念和流程,主要包括:設計模式
其中,前 7 篇屬於基礎篇,分別介紹 Flutter 中幾個最核心的概念。Rendering Pipeline 篇則是在此基礎上從 Build、Layout 到 Paint 的流程將它們串起來,分析 Flutter UI 是如何建立的、如何更新的。最後,自定義 Widget 屬於回顧、實踐篇,分析自定義一個 Render Widget 至少須要哪些步驟。緩存
如下圖所示,Flutter 總體分爲三層:Framework (dart)、Engine (C/C++)、Embedder (Platform),上述文章主要集中在 Framework 這一層。 app
Everything’s a widget.less
在開發 Flutter 應用過程當中,接觸最多的無疑就是Widget
,是『描述』 Flutter UI 的基本單元,經過Widget
能夠作到:異步
Widget
嵌套);font
、color
等);padding
、center
等);Google 在設計Widget
時,還賦予它一些鮮明的特色:ide
聲明式 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
基類中有 3 個重要的方法 (屬性):
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 時會重點分析。
無狀態-組合型 Widget,由其build
方法描述組合 UI 的層級結構。在其生命週期內狀態不可變。
ps: 對於有父子關係的類,在子類中只會介紹新增或有變化的方法
StatelessElement createElement() ——「Stateless Widget」對應的 Element 爲StatelessElement
,通常狀況下StatelessWidget
子類沒必要重寫該方法,即子類對應的 Element 也是StatelessElement
;
Widget build(BuildContext context) —— 算是 Flutter 體系中的核心方法之一,以『聲明式 UI』的形式描述了該組合式 Widget 的 UI 層級結構及樣式信息,也是開發 Flutter 應用的主要工做『場所』。該方法在 3 種狀況下被調用:
當「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 時受影響的範圍。
有狀態-組合型 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 中只有一個實例。
The logic and internal state for a 「Stateful Widget」.
State 用於處理「Stateful Widget」的業務邏輯以及可變狀態。 因爲其內部狀態是可變的,故 State 有較複雜的生命週期:
如上圖,State 的生命週期大體能夠分爲 8 個階段: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 棵樹以及更新流程在後續文章中會有詳細介紹
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;didUpdateWidget
中先取消舊的訂閱,再執行新的訂閱;dispose
中執行 unsubscribe。在State.initState
方法中不能調用BuildContext.dependOnInheritedWidgetOfExactType
,但State.didChangeDependencies
會隨之執行,在該方法中能夠調用。
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』的信息。
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;
複製代碼
真正與渲染相關的 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
。
@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
。
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
)。
下篇再見!