深入淺出 Flutter Framework 之 Widget

峰之巔 發表於 2020-05-10

本文是『 深入淺出 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 這一層。

深入淺出 Flutter Framework 之 Widget

Widget


Everything’s a widget.

在開發 Flutter 應用過程中,接觸最多的無疑就是Widget,是『描述』 Flutter UI 的基本單元,通過Widget可以做到:

  • 描述 UI 的層級結構 (通過Widget巢狀);
  • 定製 UI 的具體樣式 (如:fontcolor等);
  • 指導 UI 的佈局過程 (如:paddingcenter等);
  • ...

Google 在設計Widget時,還賦予它一些鮮明的特點:

  • 宣告式 UI —— 相對於傳統 Native 開發中的命令式 UI,宣告式 UI 有不少優勢,如:開發效率顯著提升、UI 可維護性明顯加強等;

  • 不可變性 —— Flutter 中所有Widget都是不可變的(immutable),即其內部成員都是不可變的(final),對於變化的部分需要通過「Stateful Widget-State」的方式實現;

  • 組合大於繼承 —— Widget設計遵循組合大於繼承這一優秀的設計理念,通過將多個功能相對單一的Widget組合起來便可得到功能相對複雜的Widget

Widget類定義處有這樣一段註釋:

深入淺出 Flutter Framework 之 Widget
這段註釋闡明瞭Widget的本質:用於配置Element的,Widget本質上是 UI 的配置資訊 (附帶部分業務邏輯)。

我們通常會將通過Widget描述的 UI 層級結構稱之為「Widget Tree」,但與「Element Tree」、「RenderObject Tree」以及「Layer Tree」相比,實質上並不存在「Widget Tree」。為了描述方便,將 Widget 組合描述的 UI 層級結構稱之為「Widget Tree」,也未嘗不可。

分類

深入淺出 Flutter Framework 之 Widget
如上圖所示,按照功能劃分Widget大致可以分為 3 類:

  • 「Component Widget」 —— 組合類 Widget,這類 Widget 都直接或間接繼承於StatelessWidgetStatefulWidget,上一小節提到過在 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 的基類。

深入淺出 Flutter Framework 之 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個WidgetruntimeTypekey都相等時,返回true,即可以直接更新 (key 為 null 時,認為相等)。

上述更新流程,同樣在介紹 Element 時會重點分析。

StatelessWidget

無狀態-組合型 Widget,由其build方法描述組合 UI 的層級結構。在其生命週期內狀態不可變。

深入淺出 Flutter Framework 之 Widget

深入淺出 Flutter Framework 之 Widget

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中。

深入淺出 Flutter Framework 之 Widget

深入淺出 Flutter Framework 之 Widget

  • 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 有較複雜的生命週期:

深入淺出 Flutter Framework 之 Widget
如上圖,State 的生命週期大致可以分為 8 個階段:

  • 在對應的「Stateful Element」被掛載 (mount) 到樹上時,通過StatefulElement.constructor --> StatefulWidget.createState建立 State 例項;

StatefulElement.constructor中的_state._element = this;可知,State._emelent指向了對應的 Element 例項,而我們熟知的State.context引用的就是這個_elementBuildContext get context => _element;State例項與Element例項間的繫結關係一經確定,在整個生命週期內不會再變了 (Element 對應的 Widget 可能會變,但對應的 State 永遠不會變),期間,Element可以在樹上移動,但上述關係不會變 (即「Stateful Element」是帶著狀態移動的)。

  • StatefulElement 在掛載過程中接著會呼叫State.initState,子類可以重寫該方法執行相關的初始化操作 (此時可以引用contextwidget屬性);

  • 同樣在掛載過程中會呼叫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方法將報錯。子類重寫該方法以釋放任何佔用的資源。

深入淺出 Flutter Framework 之 Widget
至此,State 中的核心方法基本都已在上述過程中介紹了,下面重點看一下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方法依賴了自身狀態會變化的物件,如:ChangeNotifierStream或其他可以被訂閱的物件,需要確保在initStatedidUpdateWidgetdispose等 3 方法間有正確的訂閱 (subscribe) 與取消訂閱 (unsubscribe) 的操作:

    • initState中執行 subscribe;
    • 如果關聯的「Stateful Widget」與訂閱有關,在didUpdateWidget中先取消舊的訂閱,再執行新的訂閱;
    • dispose中執行 unsubscribe。
  • State.initState方法中不能呼叫BuildContext.dependOnInheritedWidgetOfExactType,但State.didChangeDependencies會隨之執行,在該方法中可以呼叫。

ParentDataWidget

ParentDataWidget以及下面要介紹的InheritedElement都繼承自ProxyWidget,由於ProxyWidget作為抽象基類本身沒有任何功能,故下面直接介紹ParentDataWidgetInheritedElement

深入淺出 Flutter Framework 之 Widget
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

深入淺出 Flutter Framework 之 Widget

來看個例子,通常配合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』的資訊。

深入淺出 Flutter Framework 之 Widget

InheritedWidget

深入淺出 Flutter Framework 之 Widget
InheritedWidget 用於在樹上向下傳遞資料。 通過BuildContext.dependOnInheritedWidgetOfExactType可以獲取最近的「Inherited Widget」,需要注意的是通過這種方式獲取「Inherited Widget」時,當「Inherited Widget」狀態有變化時,會導致該引用方 rebuild。

具體原理在介紹 Element 時會詳細分析。

通常,為了使用方便會「Inherited Widget」會提供靜態方法of,在該方法中呼叫BuildContext.dependOnInheritedWidgetOfExactTypeof方法可以直接返回「Inherited Widget」,也可以是具體的資料。

有時,「Inherited Widget」是作為另一個類的實現細節而存在的,其本身是私有的(外部不可見),此時of方法就會放到對外公開的類上。最典型的例子就是Theme,其本身是StatelessWidget型別,但其內部建立了一個「Inherited Widget」:_InheritedThemeof方法就定義在上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;
}
複製程式碼

深入淺出 Flutter Framework 之 Widget

  • 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 上。

深入淺出 Flutter Framework 之 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

FlexRowColumn的基類,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的幾個子類:LeafRenderObjectWidgetSingleChildRenderObjectWidgetMultiChildRenderObjectWidget只是重寫了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)。

下篇再見!