前言
使用过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基类四个子类分别是
- StatelessWidget
- StatefulWidget
- RenderObjectWidget
- 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<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则相对复杂许多。
- 有不少生命周期钩子函数
- 有状态存储对象
- 有修改状态对象的函数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 <Widget>[] })
}
final List<Widget> children;
@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<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渲染原理,感兴趣的朋友可以关注一波~