Flutter核心类分析深入理解Widget
Posted 牧羊人.阿标
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter核心类分析深入理解Widget相关的知识,希望对你有一定的参考价值。
文章目录
背景
相信我们在Flutter开发过程中接触最多的无疑就是Widget了,通过Widget我们可以实现诸多功能:
- 描述UI的层级结构
- 定制UI的各色样式,国际化(font, color, theme)
- 定义UI布局方式(padding,center等)
- 数据共享(事件传递)(notification,InheritedWidget等)
按照官方的说法“Widget是用于描述Element的配置信息”,如何去理解这句话,下面我们具体分析:
Widgets系统分类
通过源码查看,我们发现Widget是一个抽象类,直接或者间接继承自Widget的类达到六百多个,下面整合这些Widget信息对Widget做一个简单的分类,直接继承自Widget的类只有四个(还有一个PreferredSizeWidget,在我们实际开发者用得比较少),我们就以这四个作为入口:
上图所列出来的都是直接或者间接继承自Widget的抽象类,实际开发中用到的基本上都是继承自这些抽象类,总体来说这些类大致分为三部分:
-
Components Widget
组合类Widget,这类Widget直接或者间接继承自StatelessWidget、StatefulWidget。
Flutter遵循组合大于继承的原则,通过组合相对单一的Widget可以得到功能复杂的Widget,我们平常所使用的的各种Widget,比如:Container,Text,Scaffold等,都属于这类Widget;
-
Renderer Widget
渲染类Widget,是最核心的Widget类型,会直接参与Flutter UI界面的布局,绘制流程。其实Components Widget最终也是由这类Widget组合而成。
无论是Component Widget还是Proxy Widget最终都会映射到Renderer Widget上,否则将无法绘制到屏幕上。
这三类Widget只有Renderer Widget有与之一一对应的Render Object。
-
Proxy WIdget
代理类WIdget,如其名,它并不设计Widget内部逻辑,只是为Child Widget提供一些附件的中间功能。往往是将Widget附加到当前Proxy Widget的Child属性上,实现信息传递与共享。
如;InheritedWidget用于从父Widget到子Widget信息传递,ParentDataWidget用于配置布局信息传递。
核心源码
Widget
Widget
是所有Widget的基类
@immutable
abstract class Widget extends DiagnosticableTree {
/// Initializes [key] for subclasses.
const Widget({ this.key });
final Key? key;
@protected
@factory
Element createElement();
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
}
Widget是一个immutable类,继承自DiagnosticableTree
,DiagnosticableTree
这个类,主要是用于在调试的时候获取子类的各种属性和children信息,这里暂且不管它。
通过源码我们看到它的三个核心部分(属性和方法)
-
Key
在同一父节点下,用作兄弟节点间的唯一标识,主要用于(结合下面
canUpdate
方法)控制当 Widget 更新时,对应的 Element 如何处理 (是更新还是新建); -
Element createElement()
每个
Widget
都有一个与之对应的Element
,由该方法负责创建,createElement
可以理解为设计模式中的工厂方法,具体的Element
类型由对应的Widget
子类负责创建; -
bool canUpdate(…)
canUpdate
其实我们在深入理解Key一文中已经见识过了。主要是判断是否可以用 new widget 修改前一帧用 old widget 生成的 Element,而不是创建新的 Element,Widget
类的默认实现为:2个Widget
的runtimeType
与key
都相等时,返回true
,即可以直接更新 (key 为 null 时,认为相等)。
StatelessWidget
abstract class StatelessWidget extends Widget {
/// Initializes [key] for subclasses.
const StatelessWidget({ Key? key }) : super(key: key);
@override
StatelessElement createElement() => StatelessElement(this);
@protected
Widget build(BuildContext context);
}
无状态Component Widget,由build构建组合Widget层次结构,在它生命周期内不可变。
方法解析
-
StatelessElement createElement()
一般情况下子类不需要重写该方法,子类对应的Element也是StatelessElement是ComponentElement的一种
-
Widget build(BuildContext context)
核心方法,构建该组合式 Widget 的 UI 层级结构及样式信息。该方法通常只在一下三种情况下调用:
- Widget 第一次被加入到 Widget Tree 中 (更准确地说是其对应的 Element 被加入到 Element Tree 时,即 Element 被挂载『mount』时);
Parent Widget
修改了其配置信息;- 该 Widget 依赖的
Inherited Widget
发生变化时。
当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构造方法;
- 必要时,可以将
Stateless Widget
重构成Stateful Widget
,以便可以使用Stateful Widget
中一些特定的优化手法,如:缓存sub trees
的公共部分,并在改变树结构时使用GlobalKey; - 尽量减小 rebuilt 范围,如:某个 Widget 因使用了
Inherited Widget
,导致频繁 rebuilt,可以将真正依赖Inherited Widget
的部分提取出来,封装成更小的独立 Widget,并尽量将该独立 Widget 推向树的叶子节点,以便减小 rebuilt 时受影响的范围。
StatefulWidget
abstract class StatefulWidget extends Widget {
/// Initializes [key] for subclasses.
const StatefulWidget({ Key? key }) : super(key: key);
@override
StatefulElement createElement() => StatefulElement(this);
@protected
@factory
State createState(); // ignore: no_logic_in_create_state, this is the original sin
}
有状态Component Widget,它也是immutable类,本身是不可变的。其可变的状态存在于State
中。
方法解析
-
StatefulElement createElement()
StatefulWidget
对应的Element为StatefulElement
,一般情况下也不需要重写该方法,所以子类对于的Element也是StatefulElement
是ComponentElement的一种。 -
State createState()
创建对应的 State,该方法在
StatefulElement
的构造方法中被调用。可以简单地理解为当Stateful Widget
被添加到 Widget Tree 时会调用该方法。如果是更新Element并不会调用该方法。class StatefulElement extends ComponentElement { // 构造方法中调用createState StatefulElement(StatefulWidget widget) : state = widget.createState(), super(widget) { } }
实际上是Stateful Widget
对应的Stateful Element
被添加到 Element Tree 时,伴随Stateful Element
的初始化,createState
方法被调用。一个 Widget 实例可以对应多个 Element 实例 (也就是同一份配置信息 (Widget) 可以在 Element Tree 上不同位置配置多个 Element 节点),因此,createState
方法在Stateful Widget
生命周期内可能会被调用多次。
另外,需要注意的是配有GlobalKey
的 Widget 对应的 Element 在整个 Element Tree 中只有一个实例。
State
State
是用于描述StatefulWidget
的业务逻辑和内部状态。创建时机上面已经讲过,这里需要注意的是如果从树中移除一个StatefulWidget并稍后再次插入到树中,那么framework将会再次调用StatefulWidget.createState来创建一个新的State对象,如果不移除只是update的话是不会再次调用createState的。
其生命周期:
- 框架通过调用
StatefulWidget.createState
创建一个State
对象 。 - 新创建的
State
对象与BuildContext
相关联。这种关联是永久性的:State
对象永远不会改变它的BuildContext
。但是,BuildContext
本身可以与其子树一起在树周围移动。此时State
对象被认为是mount
。 - 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 的变化; - 在 UI 更新过程中,任何节点都有被移除的可能,State 也会随之移除,(如上一步中
runtimeType
||key
不相等时)。此时会调用State.deactivate
方法,由于被移除的节点可能会被重新插入树中某个新的位置上,故子类重写该方法以清理与节点位置相关的信息 (如:该 State 对其他 element 的引用)、同时,不应在该方法中做资源清理; - 当节点被重新插入树中时,
State.build
方法被再次调用; - 对于在当前帧动画结束时尚未被重新插入的节点,
State.dispose
方法被执行,State 生命周期随之结束,此后再调用State.setState
方法将报错。子类重写该方法以释放任何占用的资源。
setState
void setState(VoidCallback fn) {
_element!.markNeedsBuild();
}
setState
方法很简单,去掉冗余的assert信息,其实只有一行代码就是调用_element.markNeedsBuild()
方法。_element.markNeedsBuild
方法后面Element类分析的时候再讲解。
setState
方法有几个点值得关注(通过断言assert分析得出):
- 在
State.dispose
之后,不能再调用setState - 在State的构造方法中不能调用
setState
setState
方法的回调函数(fn)不能是异步(返回值为Future)。- 通过
setState
之所以能更新UI,是因为内部调用了_element.markNeedsBuild()
,间接调用了onBuildScheduled
。
InheritedWidget
InheritedWidget
在之前的文章深入理解数据共享InheritedWidget已经讲解过,这里不再重复。
RenderObjectWidget
RenderObjectWidget
为RenderObjectlements
提供配置信息,通过包装RenderObjects
提供实际渲染需要的数据。一切其他类型的Widget
知道它要渲染到屏幕上,最终都要回归到该类型的Widget上。
abstract class RenderObjectWidget extends Widget {
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) { }
}
核心方法就只有四个:
-
createElement()
RenderObjectWidget
对应的Element为RenderObjectElement
,由于RenderObjectElement
也是抽象类,所以子类需要重写该方法。 -
createRenderObject(BuildContext context)
核心方法,创建 Render Widget 对应的 Render Object,同样子类需要重写该方法。该方法在对应的 Element 被挂载到树上时调用(
Element.mount
),即在 Element 挂载过程中同步构建了Render Tree
-
updateRenderObject(BuildContext context, covariant RenderObject renderObject)
核心方法,在 Widget 更新后,修改对应的 Render Object。该方法在首次 build 以及需要更新 Widget 时都会调用;
-
didUnmountRenderObject(covariant RenderObject renderObject)
对应的
Render Object
从Render Tree
上移除时调用该方法。
总结
好了,到了这里Widget介绍总算结束,这里做个总结:
- Widget本质实际上就是UI的配置信息,本身是immutable的。
- Widget 从功能上可以分为 3 类:
Component Widget
、Proxy Widget
以及Renderer Widget
- Widget 与 Element 一一对应
- StatefulWidget 新创建的
State
对象与BuildContext
相关联。这种关联是永久性的:State
对象永远不会改变它的BuildContext
。但是,BuildContext
本身可以与其子树一起在树周围移动。此时State
对象被认为是mount
。 - 只有
Renderer Widget
才会参与最终的 UI 生成过程(Layout、Paint),只有该类型的 Widget 才有与之对应的Render Object
,同样由其提供创建方法(createRenderObject
)。
以上是关于Flutter核心类分析深入理解Widget的主要内容,如果未能解决你的问题,请参考以下文章
Flutter核心类分析深入理解数据共享Notification
Flutter核心类分析深入理解数据共享Notification
Flutter核心类分析深入理解数据共享InheritedWidget