Flutter Widget原理解讀(一)

語言: CN / TW / HK

前言

使用過Flutter的同學,應該都聽過一句話“everything is a widget——在Flutter中萬物皆是Widget”。

雖然不能說在Flutter開發中所有程式碼模組都是一個Widget,但足以說明Widget在Flutter中的重要性,本篇文章就重點關於Flutter Widget的原理進行解讀。

Widget簡介

什麼是Widget?我們先看一下官方的描述

“==Describes the configuration for an [Element]==”

在Flutter中,Widget的功能是“描述一個UI元素的配置資料”。

這句話很簡單,如何理解呢?暫時可以簡單的理解,FLutter最終繪製在裝置上的顯示元素,都是通過Widget配置出來的。

在web前端開發中,我們知道瀏覽器頁面由HTML+CSS+JS配置而成,其中HTML負責配置UI結構,CSS負責配置UI樣式,JS負責UI的互動。

而在Flutter中,無論是UI結構,還是UI樣式,再到UI互動都是通過Widget完成。例如:

  1. Widget樹結構配置UI結構
  2. 樣式Widget,Padding、Color等
  3. 互動Widget,GestureDetector等

Widget分類

在Flutter中,官方提供的原生Widget多達300+,這麼多Widget,在基礎原理層面是如何分類的呢?

使用過Flutter的同學,最熟悉的應該是StatelessWidget和StatefulWidget兩種Widget,除了這兩種還要其他的嗎?

我們來看一下Flutter Widget元件繼承圖。

image

從上圖中,我們知道繼承Widget基類四個子類分別是 1. StatelessWidget 2. StatefulWidget 3. RenderObjectWidget 4. ProxyWidget

其中前三類StatelessWidget、StatefulWidget、RenderObjectWidget負責UI渲染配置,而ProxyWidget繼承的子類InheritedWidget負責Widget樹向下傳遞資料。

如果按照功能來分類,則可分成兩大類:

  1. UI渲染配置Widget:StatelessWidget、StatefulWidget、RenderObjectWidget
  2. UI樹資料狀態管理Widget:InheritedWidget

StatelessWidget、StatefulWidget、RenderObjectWidget又可依據UI配置型別Widget,分成兩類:

  1. 組合Widget:StatelessWidget、StatefulWidget
  2. 自定義渲染Widget:RenderObjectWidget

接下來,本篇文章主要講解UI配置型別Widget,UI樹資料狀態管理Widget——InheritedWidget,將在下一篇文章中講解。

組合Widget自定義渲染Widget區別?

在日常業務開發中,開發者只需要使用組合Widget就能 滿足99%的業務功能,所以對於初學Flutter的同學來說,學會StatelessWidget與StatefulWidget的使用就能滿足業務開發需求。

組合Widget與自定義渲染Widget有什麼區別呢?

站在前端的角度,我們開發一個HTML頁面,只需要使用W3C定義的標準的div、span等標籤和css樣式position、color等即可搭建一個完整的頁面。

至於div、color瀏覽器最終是如何渲染的,無需開發者定義實現,全權由瀏覽器引擎原生實現。開發者基於div+css開發的元件都屬於組合元件,等同於組合Widget。

那什麼是自定義渲染Widget呢?就好比,瀏覽器未支援css3之前,如果要實現邊框圓角樣式“border-radius”使用css是做不到的。假如瀏覽器提供前端開發者自定義css樣式渲染的介面,由前端開發者實現邊框圓角的css渲染,則屬於自定義渲染元件,等同於與自定義渲染Widget。

組合Widget,StatelessWidget與StatefulWidget

我們先看看,原始碼抽象類的定義

StatelessWidget原始碼

abstract class StatelessWidget extends Widget { const StatelessWidget({ Key? key }) : super(key: key); @override StatelessElement createElement() => StatelessElement(this); @protected Widget build(BuildContext context); }

StatefulWidget

``` abstract class StatefulWidget extends Widget { const StatefulWidget({ Key? key }) : super(key: key);

@override StatefulElement createElement() => StatefulElement(this);

@protected @factory State createState(); // ignore: } ```

從原始碼我們可以看出,StatelessWidget是一個無狀態元件,提供一個元件構建函式build。StatefulWidget是一個有狀態元件,提供一個狀態建立函式createState。

接下來看看StatefulWidget類中依賴State類的原始碼

``` abstract class State with Diagnosticable { T get widget => _widget!; T? _widget; BuildContext get context { assert(() { if (_element == null) { throw FlutterError( 'This widget has been unmounted, so the State no longer has a context (and should be considered defunct). \n' 'Consider canceling any active work during "dispose" or using the "mounted" getter to determine if the State is still active.', ); } return true; }()); return _element!; } StatefulElement? _element; bool get mounted => _element != null; @protected @mustCallSuper void initState() {} @mustCallSuper @protected void didUpdateWidget(covariant T oldWidget) { } @protected @mustCallSuper void reassemble() { }

@protected void setState(VoidCallback fn) { _element!.markNeedsBuild(); }

@protected @mustCallSuper void deactivate() { }

@protected @mustCallSuper void activate() { }

@protected @mustCallSuper void dispose() { } ```

從上面程式碼中可以看出,State是一個有狀態的元件,有生命週期鉤子函式initState、dispose等和狀態改變函式setState。

@protected void setState(VoidCallback fn) { _element!.markNeedsBuild(); }

從從setState原始碼定義可以知道,setState會觸發元件重渲染函式markNeedsBuild。

從原始碼對比來看StatelessWidget實現非常簡單,連元件生命週期的鉤子函式都沒有,而StatefullWidget則相對複雜許多。

  1. 有不少生命週期鉤子函式
  2. 有狀態儲存物件
  3. 有修改狀態物件的函式setState

如果用React元件類比,則StatelessWidget相當於純函式元件,而StatefullWidget則是類元件。

且StatelessWidget和StatefullWidget使用場景也跟React純函式元件和類元件使用場景相同,在此不做贅述。

StatefulWidget生命週期流程圖 image

重點關注如下生命週期鉤子:

  1. initState():widget 第一次插入 widget 樹呼叫,此時還沒有觸發build函式,且整個state生命週期只調用一次
  2. didUpdateWidget():當State物件的狀態發生變化時,重新build之前呼叫,一般在這裡判斷哪些狀態變化是需要觸發哪些業務函式時呼叫。
  3. dispose():當 State 物件從樹中被永久移除時呼叫,通常在此回撥中釋放資源。

自定義渲染Widget——RenderObjectWidget

我們先看看RenderObjectWidget子類繼承關係圖。

image

從上圖可以得知RenderObjectWidget分成三類:

  1. LeafRenderObjectWidget
  2. SingleChildRenderObjectWidget
  3. MultiChildRenderObjectWidget

Flutter原生基礎佈局元件都是通過繼承SingleChildRenderObjectWidget或 MultiChildRenderObjectWidget實現。

接下來我們看看原始碼實現:

RenderObjectWidget ``` abstract class RenderObjectWidget extends Widget { provide const RenderObjectWidget({ Key? key }) : super(key: key);

@override @factory RenderObjectElement createElement();

@protected @factory RenderObject createRenderObject(BuildContext context);

@protected void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

@protected void didUnmountRenderObject(covariant RenderObject renderObject) { } } ```

LeafRenderObjectWidget abstract class LeafRenderObjectWidget extends RenderObjectWidget { provide const LeafRenderObjectWidget({ Key? key }) : super(key: key); @override LeafRenderObjectElement createElement() => LeafRenderObjectElement(this); } SingleChildRenderObjectWidget

``` abstract class SingleChildRenderObjectWidget extends RenderObjectWidget { provide const SingleChildRenderObjectWidget({ Key? key, this.child }) : super(key: key);

final Widget? child;

@override SingleChildRenderObjectElement createElement() => SingleChildRenderObjectElement(this); } ```

MultiChildRenderObjectWidget ``` abstract class MultiChildRenderObjectWidget extends RenderObjectWidget { MultiChildRenderObjectWidget({ Key? key, this.children = const [] }) } final List children;

@override MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this); } ```

從原始碼可以看出LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget處理RenderObjectWidget個數有差異。

  1. SingleChildRenderObjectWidget:處理單個RenderObjectWidget。
  2. MultiChildRenderObjectWidget:處理多個RenderObjectWidget。
  3. LeafRenderObjectWidget:葉子渲染Widget,處理沒有children的RenderObjectWidget。

而繼承RenderObjectWidget的自定義子類最重要是需要實現抽象函式createRenderObject、updateRenderObject,對應建立、更新

拿Padding原生Widget原始碼實現距離。

``` class Padding extends SingleChildRenderObjectWidget { /// Creates a widget that insets its child. /// /// The [padding] argument must not be null. const Padding({ Key? key, required this.padding, Widget? child, }) : assert(padding != null), super(key: key, child: child);

/// The amount of space by which to inset the child. final EdgeInsetsGeometry padding;

@override RenderPadding createRenderObject(BuildContext context) { return RenderPadding( padding: padding, textDirection: Directionality.maybeOf(context), ); }

@override void updateRenderObject(BuildContext context, RenderPadding renderObject) { renderObject ..padding = padding ..textDirection = Directionality.maybeOf(context); }

@override void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties.add(DiagnosticsProperty('padding', padding)); } } ```

從原始碼實現來看,傳入Padding Widget的子Widget直接傳遞到父SingleChildRenderObjectWidget child,而Padding只是實現Widget容器佈局RenderPadding createRenderObject(BuildContext context),具體實現需要看RenderPadding實現原始碼,如下:

``` class RenderPadding extends RenderShiftedBox { /// Creates a render object that insets its child. /// /// The [padding] argument must not be null and must have non-negative insets. RenderPadding({ required EdgeInsetsGeometry padding, TextDirection? textDirection, RenderBox? child, }) : assert(padding != null), assert(padding.isNonNegative), _textDirection = textDirection, _padding = padding, super(child);

EdgeInsets? _resolvedPadding;

void _resolve() { if (_resolvedPadding != null) return; _resolvedPadding = padding.resolve(textDirection); assert(_resolvedPadding!.isNonNegative); }

void _markNeedResolution() { _resolvedPadding = null; markNeedsLayout(); }

/// The amount to pad the child in each dimension. /// /// If this is set to an [EdgeInsetsDirectional] object, then [textDirection] /// must not be null. EdgeInsetsGeometry get padding => _padding; EdgeInsetsGeometry _padding; set padding(EdgeInsetsGeometry value) { assert(value != null); assert(value.isNonNegative); if (_padding == value) return; _padding = value; _markNeedResolution(); }

/// The text direction with which to resolve [padding]. /// /// This may be changed to null, but only after the [padding] has been changed /// to a value that does not depend on the direction. TextDirection? get textDirection => _textDirection; TextDirection? _textDirection; set textDirection(TextDirection? value) { if (_textDirection == value) return; _textDirection = value; _markNeedResolution(); }

@override double computeMinIntrinsicWidth(double height) { _resolve(); final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (child != null) // next line relies on double.infinity absorption return child!.getMinIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding; return totalHorizontalPadding; }

@override double computeMaxIntrinsicWidth(double height) { _resolve(); final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (child != null) // next line relies on double.infinity absorption return child!.getMaxIntrinsicWidth(math.max(0.0, height - totalVerticalPadding)) + totalHorizontalPadding; return totalHorizontalPadding; }

@override double computeMinIntrinsicHeight(double width) { _resolve(); final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (child != null) // next line relies on double.infinity absorption return child!.getMinIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding; return totalVerticalPadding; }

@override double computeMaxIntrinsicHeight(double width) { _resolve(); final double totalHorizontalPadding = _resolvedPadding!.left + _resolvedPadding!.right; final double totalVerticalPadding = _resolvedPadding!.top + _resolvedPadding!.bottom; if (child != null) // next line relies on double.infinity absorption return child!.getMaxIntrinsicHeight(math.max(0.0, width - totalHorizontalPadding)) + totalVerticalPadding; return totalVerticalPadding; }

@override Size computeDryLayout(BoxConstraints constraints) { _resolve(); assert(_resolvedPadding != null); if (child == null) { return constraints.constrain(Size( _resolvedPadding!.left + _resolvedPadding!.right, _resolvedPadding!.top + _resolvedPadding!.bottom, )); } final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!); final Size childSize = child!.getDryLayout(innerConstraints); return constraints.constrain(Size( _resolvedPadding!.left + childSize.width + _resolvedPadding!.right, _resolvedPadding!.top + childSize.height + _resolvedPadding!.bottom, )); }

@override void performLayout() { final BoxConstraints constraints = this.constraints; _resolve(); if (child == null) { size = constraints.constrain(Size( _resolvedPadding!.left + _resolvedPadding!.right, _resolvedPadding!.top + _resolvedPadding!.bottom, )); return; } final BoxConstraints innerConstraints = constraints.deflate(_resolvedPadding!); child!.layout(innerConstraints, parentUsesSize: true); final BoxParentData childParentData = child!.parentData! as BoxParentData; childParentData.offset = Offset(_resolvedPadding!.left, _resolvedPadding!.top); size = constraints.constrain(Size( _resolvedPadding!.left + child!.size.width + _resolvedPadding!.right, _resolvedPadding!.top + child!.size.height + _resolvedPadding!.bottom, )); } ... } ```

其中抽象類RenderShiftedBox繼承Flutter實現盒子佈局RenderBox抽象類,從原始碼可以看出,RenderPadding通過computeMinIntrinsicWidth、computeMinIntrinsicHeight、computeDryLayout、performLayout函式Padding Widget實現盒子佈局邏輯。

通過Padding Widget實現原來講解,相信大家對RenderObjectWidget實現的基本原理有一定的瞭解,其他RenderObjectWidget的實現基本相似,本文就不做一一展開,有興趣的同學可以自行閱讀Flutter原始碼。

結尾

Flutter Widget原理解讀第一部分基本到這就結束了,接下來第二章會講解InheritedWidget的實現原理。後續還會繼續深入講解Flutter三棵樹Widget樹、Element樹、RenderObject樹徹底從原理層面剖析Flutter渲染原理,感興趣的朋友可以關注一波~