Flutter Widget原理解讀(一)
前言
使用過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完成。例如:
- Widget樹結構配置UI結構
- 樣式Widget,Padding、Color等
- 互動Widget,GestureDetector等
Widget分類
在Flutter中,官方提供的原生Widget多達300+,這麼多Widget,在基礎原理層面是如何分類的呢?
使用過Flutter的同學,最熟悉的應該是StatelessWidget和StatefulWidget兩種Widget,除了這兩種還要其他的嗎?
我們來看一下Flutter Widget元件繼承圖。
從上圖中,我們知道繼承Widget基類四個子類分別是 1. StatelessWidget 2. StatefulWidget 3. RenderObjectWidget 4. ProxyWidget
其中前三類StatelessWidget、StatefulWidget、RenderObjectWidget負責UI渲染配置,而ProxyWidget繼承的子類InheritedWidget負責Widget樹向下傳遞資料。
如果按照功能來分類,則可分成兩大類:
- UI渲染配置Widget:StatelessWidget、StatefulWidget、RenderObjectWidget
- UI樹資料狀態管理Widget:InheritedWidget
StatelessWidget、StatefulWidget、RenderObjectWidget又可依據UI配置型別Widget,分成兩類:
- 組合Widget:StatelessWidget、StatefulWidget
- 自定義渲染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
@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則相對複雜許多。
- 有不少生命週期鉤子函式
- 有狀態儲存物件
- 有修改狀態物件的函式setState
如果用React元件類比,則StatelessWidget相當於純函式元件,而StatefullWidget則是類元件。
且StatelessWidget和StatefullWidget使用場景也跟React純函式元件和類元件使用場景相同,在此不做贅述。
StatefulWidget生命週期流程圖
重點關注如下生命週期鉤子:
- initState():widget 第一次插入 widget 樹呼叫,此時還沒有觸發build函式,且整個state生命週期只調用一次
- didUpdateWidget():當State物件的狀態發生變化時,重新build之前呼叫,一般在這裡判斷哪些狀態變化是需要觸發哪些業務函式時呼叫。
- dispose():當 State 物件從樹中被永久移除時呼叫,通常在此回撥中釋放資源。
自定義渲染Widget——RenderObjectWidget
我們先看看RenderObjectWidget子類繼承關係圖。
從上圖可以得知RenderObjectWidget分成三類:
- LeafRenderObjectWidget
- SingleChildRenderObjectWidget
- 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
@override MultiChildRenderObjectElement createElement() => MultiChildRenderObjectElement(this); } ```
從原始碼可以看出LeafRenderObjectWidget、SingleChildRenderObjectWidget、MultiChildRenderObjectWidget處理RenderObjectWidget個數有差異。
- SingleChildRenderObjectWidget:處理單個RenderObjectWidget。
- MultiChildRenderObjectWidget:處理多個RenderObjectWidget。
- 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 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渲染原理,感興趣的朋友可以關注一波~