本文是『 深刻浅出 Flutter Framework 』系列文章的第一篇,主要以不一样类型 Widget 的核心方法为切入点,对其展开详细分析。html
本文同时发表于个人我的博客git
Flutter 做为一种新兴跨平台解决方案,自 2017 年 Google 在 I/O 大会上推出后,尤为是在 2018 年 I/O 大会上发布第一个预览版后,迅速引发移动开发者的普遍关注,并成为时下最热门的跨平台解决方案 ( 没有之一 ) !github
本系列文章将深刻 Flutter Framework 内部逐步去分析其核心概念和流程,主要包括:设计模式
其中,前 7 篇属于基础篇,分别介绍 Flutter 中几个最核心的概念。Rendering Pipeline 篇则是在此基础上从 Build、Layout 到 Paint 的流程将它们串起来,分析 Flutter UI 是如何建立的、如何更新的。最后,自定义 Widget 属于回顾、实践篇,分析自定义一个 Render Widget 至少须要哪些步骤。缓存
如下图所示,Flutter 总体分为三层:Framework (dart)、Engine (C/C++)、Embedder (Platform),上述文章主要集中在 Framework 这一层。 app
![]()
Everything’s a widget.less
在开发 Flutter 应用过程当中,接触最多的无疑就是Widget
,是『描述』 Flutter UI 的基本单元,经过Widget
能够作到:异步
Widget
嵌套);font
、color
等);padding
、center
等);Google 在设计Widget
时,还赋予它一些鲜明的特色:ide
声明式 UI —— 相对于传统 Native 开发中的命令式 UI,声明式 UI 有很多优点,如:开发效率显著提高、UI 可维护性明显增强等;函数
不可变性 —— Flutter 中全部Widget
都是不可变的(immutable),即其内部成员都是不可变的(final
),对于变化的部分须要经过「Stateful Widget-State」的方式实现;
组合大于继承 —— Widget
设计遵循组合大于继承这一优秀的设计理念,经过将多个功能相对单一的Widget
组合起来即可获得功能相对复杂的Widget
。
在Widget
类定义处有这样一段注释:
Widget
的本质:
用于配置Element
的,Widget
本质上是 UI 的配置信息 (附带部分业务逻辑)。
咱们一般会将经过
Widget
描述的 UI 层级结构称之为「Widget Tree」,但与「Element Tree」、「RenderObject Tree」以及「Layer Tree」相比,实质上并不存在「Widget Tree」。为了描述方便,将 Widget 组合描述的 UI 层级结构称之为「Widget Tree」,也何尝不可。
Widget
大体能够分为 3 类:
「Component Widget」 —— 组合类 Widget,这类 Widget 都直接或间接继承于StatelessWidget
或StatefulWidget
,上一小节提到过在 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
基类中有 3 个重要的方法 (属性):
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个Widget
的runtimeType
与key
都相等时,返回true
,便可以直接更新 (key 为 null 时,认为相等)。
上述更新流程,一样在介绍 Element 时会重点分析。
无状态-组合型 Widget,由其build
方法描述组合 UI 的层级结构。在其生命周期内状态不可变。
ps: 对于有父子关系的类,在子类中只会介绍新增或有变化的方法
StatelessElement createElement() ——「Stateless Widget」对应的 Element 为StatelessElement
,通常状况下StatelessWidget
子类没必要重写该方法,即子类对应的 Element 也是StatelessElement
;
Widget build(BuildContext context) —— 算是 Flutter 体系中的核心方法之一,以『声明式 UI』的形式描述了该组合式 Widget 的 UI 层级结构及样式信息,也是开发 Flutter 应用的主要工做『场所』。该方法在 3 种状况下被调用:
当「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 时受影响的范围。
有状态-组合型 Widget,但要注意的是StatefulWidget
自己仍是不可变的,其可变状态存在于State
中。
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 中只有一个实例。
The logic and internal state for a 「Stateful Widget」.
State 用于处理「Stateful Widget」的业务逻辑以及可变状态。 因为其内部状态是可变的,故 State 有较复杂的生命周期:
StatefulElement.constructor
--> StatefulWidget.createState
建立 State 实例;从
StatefulElement.constructor
中的_state._element = this;
可知,State._emelent
指向了对应的 Element 实例,而咱们熟知的State.context
引用的就是这个_element
:BuildContext get context => _element;
。State
实例与Element
实例间的绑定关系一经肯定,在整个生命周期内不会再变了 (Element 对应的 Widget 可能会变,但对应的 State 永远不会变),期间,Element
能够在树上移动,但上述关系不会变 (即「Stateful Element」是带着状态移动的)。
StatefulElement 在挂载过程当中接着会调用State.initState
,子类能够重写该方法执行相关的初始化操做 (此时能够引用context
、widget
属性);
一样在挂载过程当中会调用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 棵树以及更新流程在后续文章中会有详细介绍
State.deactivate
方法,因为被移除的节点可能会被从新插入树中某个新的位置上,故子类重写该方法以清理与节点位置相关的信息 (如:该 State 对其余 element 的引用)、同时,不该在该方法中作资源清理;从新插入操做必须在当前帧动画结束以前
当节点被从新插入树中时,State.build
方法被再次调用;
对于在当前帧动画结束时还没有被从新插入的节点,State.dispose
方法被执行,State 生命周期随之结束,此后再调用State.setState
方法将报错。子类重写该方法以释听任何占用的资源。
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
方法依赖了自身状态会变化的对象,如:ChangeNotifier
、Stream
或其余能够被订阅的对象,须要确保在initState
、didUpdateWidget
、dispose
等 3 方法间有正确的订阅 (subscribe) 与取消订阅 (unsubscribe) 的操做:
initState
中执行 subscribe;didUpdateWidget
中先取消旧的订阅,再执行新的订阅;dispose
中执行 unsubscribe。在State.initState
方法中不能调用BuildContext.dependOnInheritedWidgetOfExactType
,但State.didChangeDependencies
会随之执行,在该方法中能够调用。
ParentDataWidget
以及下面要介绍的InheritedElement
都继承自ProxyWidget
,因为ProxyWidget
做为抽象基类自己没有任何功能,故下面直接介绍ParentDataWidget
、InheritedElement
。
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
。
来看个例子,一般配合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』的信息。
BuildContext.dependOnInheritedWidgetOfExactType
能够获取最近的「Inherited Widget」,须要注意的是经过这种方式获取「Inherited Widget」时,当「Inherited Widget」状态有变化时,会致使该引用方 rebuild。
具体原理在介绍 Element 时会详细分析。
一般,为了使用方便会「Inherited Widget」会提供静态方法of
,在该方法中调用BuildContext.dependOnInheritedWidgetOfExactType
。of
方法能够直接返回「Inherited Widget」,也能够是具体的数据。
有时,「Inherited Widget」是做为另外一个类的实现细节而存在的,其自己是私有的(外部不可见),此时of
方法就会放到对外公开的类上。最典型的例子就是Theme
,其自己是StatelessWidget
类型,但其内部建立了一个「Inherited Widget」:_InheritedTheme
,of
方法就定义在上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;
}
复制代码
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;
复制代码
真正与渲染相关的 Widget,属于最核心的类型,一切其余类型的 Widget 要渲染到屏幕上,最终都要回归到该类型的 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
。
Flex
是Row
、Column
的基类,RenderFlex
继承自RenderBox
,后者继续自RenderObject
。
@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
。
RenderObjectWidget
的几个子类:LeafRenderObjectWidget
、SingleChildRenderObjectWidget
、MultiChildRenderObjectWidget
只是重写了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
)。
下篇再见!