Flutter Widget原理解读(一)

avatar
SugarTurboS Club @SugarTurboS

前言

使用过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<T extends StatefulWidget> 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 <Widget>[] })
  }
  final List<Widget> 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<EdgeInsetsGeometry>('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渲染原理,感兴趣的朋友可以关注一波~