Flutter中的Tree(一) WidgetTree

什么是Widget?

Widget是Flutter UI的配置文件. 通过Widget, 我们可以指定Flutter UI的层级结构; 设置Flutter UI的样式和布局等等.

之所以说是配置文件, 是因为Widget本身并不代表Flutter UI的实际结构(实际上和实际结构差得还很大).

作为配置文件, Widget具有不可变(immutable)的特性. 即它的属性值不可以被修改.

Widget的核心变量和方法

Widget基类有两个核心方法–createElement()和Widget.canUpdate(), 一个核心成员变量Key key.

Key

成员变量key是Widget的身份标识. Key分为两大类–GlobalKey和LocalKey. 前者的标识是全局可见的(即所有的Widget, 甚至不是Widget的东西, 都可以通过Key获取到标识的Widget); 后者的标识只有Widget的父Widget才可见.

Widget.canUpdate()

为什么需要父Widget需要标识自己的child呢? 因为有时候父Widget会有多个child. 如果想更新某一个Widget, 那就需要先找到这个Widget. 由于多个child可能是同一个类型的, 所以只看runtimeType是不能精确找到这个Widget的, 所以还需要通过Key来标识. 换句话说, 只要有了runtimeType和key, 父Widget就可以找到特定的一个Widget来更新.这就是Widget基类的静态方法Widget.canUpdate(Widget oldWidget, Widget newWidget)的逻辑.

createElement()

createElement()方法, 顾名思义是创建Element使用的. 关于Element, 我们会在下一篇文章里介绍.

Widget的分类

Widget基类有四个抽象子类–StatelessWidget、StatefulWidget、ProxyWidget和RenderObjectWidget. 所有的Widget都是继承自这四个抽象类之一来实现的.

这里多说一句. 网上有文章将Widget的子类分成了三大部分–ComponentWidget、ProxyWidget和RenderObjectWidget, 并说StatefulWidget和StatelessWidget都是继承自ComponentWidget的. 我查看了Flutter Framework的源码. 从github上打的tag来看, 至少从最早打tag的0.0.6版本开始, StatefulWidget和StatelessWidget都是直接继承自Widget类的, 并没有ComponentWidget这个抽象子类, 所以这个说法我是存疑的.

StatelessWidget

StatelessWidget是最简单的Widget类. 它只新增了一个需要重写的方法–build(BuildContext context)方法. 这方法我们都熟悉得不能再熟悉了.

build(BuildContext context)方法调用的时机有三种:

  1. StatelessWidget对应的StatelessElement被挂载到ElementTree上时
  2. StatelessWidget的父Widget修改了它的信息
  3. StatelessWidget依赖的InheritedWidget发生了变化

我们知道, build(BuildContext context)方法是被调用得很频繁的, 所以为了提升性能, 我们一般都需要做一些优化. Google官方给出了四条优化建议:

  1. 减少UI层级. 比如Container的child就没有必要再套一个Padding了.
  2. 尽可能提供const Widget, 为Widget提供const的构造函数.
  3. 必要时可以将StatelessWidget改成StatefulWidget, 这样可以用一些StatefulWidget特有的trick
  4. 减少rebuild的范围.

这里对第2点做一个说明. Dart语言提供了const Constructor(直接翻译成常量构造函数好像不太合适, 所以引用原文). 所有通过const Constructor构造的示例会变成编译时常量. 编译时常量和运行时常量的区别在于, 编译时常量需要的计算过程在编译时就已经做完了, 运行时常量的计算过程是在程序运行的过程中去做的. 显然, 编译时常量是会对性能提升有所帮助的. 同样显然的是, 对于一个类的示例来说, 要成为编译时常量, 它必须是不可变(immutable)的, 而Widget的特性正好满足了条件.

StatefulWidget

StatefulWidget同样新增了一个需要重写的方法–createState(), 其作用就是创建了一个State(这不是废话吗).

关于State, 我们放到下一篇文章再详细介绍.

RenderObjectWidget

RenderObjectWidget, 首先就要介绍一个概念–RenderObject. 实际上, RenderObject才是页面要渲染的元素, 而这个元素就是由RenderObjectWidget创建的. 为什么文章的开头我们说WidgetTree不代表Flutter UI的实际结构, 而且实际上差得还很远. 因为WidgetTree中大部分都不是RenderObjectWidget, 这些Widget最后都不参与渲染. 关于RenderObject, 我们后面会详细介绍, 这里我们可以简单认为RenderObject就是真正被页面渲染出来的元素就可以了.

RenderObjectWidget新增了三个方法: createRenderObject(BuildContext context), updateRenderObject(BuildContext context, RenderObject renderObject)和didUnmountRenderObject(covariant RenderObject renderObject). 第一个方法需要子类自己实现, 后面两个方法默认都是空实现, 子类可以根据自己的需要来重写. 这三个方法的调用时机我们也会在下一篇文章里讲到.

根据子节点的数量, RenderObjectWidget又派生出了三个抽象子类–无子节点的LeafRenderObjectWidget、有一个子节点的SingleChildRenderObjectWidget和有多个子节点的MultiChildRenderObjectWidget.

ProxyWidget

ProxyWidget是一类有且只有一个child的Widget. 它可以为child提供一些额外的能力. 它主要的抽象子类有两个–InheritedWidget和ParentDataWidget.

InheritedWidget

InheritedWidget我们都很熟悉了. 它为child提供的是数据查找和数据监听的能力.

如果一个Widget的**祖先节点(不止是父节点)**是InheritedWidget, 那么它既可以通过BuildContext::ancestorWidgetOfExactType()来使用InheritedWidget的数据; 也可以通过BuildContext::dependOnInheritedWidgetOfExactType()来使用并监听InhertiedWidget的数据.

既然提供了监听的功能, 那么就一定有一个通知监听者的条件. 为此, 它新增了一个updateShouldNotify(InheritedWidget oldWidget)的方法. 方法很简单, 只要返回了true, 就认为InheritedWidget发生了变化, 所有监听它的Widget都会被重新build. 这里的监听机制我们也放到下一篇文章里仔细讲.

ParentDataWidget

ParentDataWidget为child提供ParentData. ParentData是RenderObject在布局child RenderObject时要用到的辅助信息. 关于RenderObject以及布局过程我们会在后面的文章中介绍, 这里我们只需要认为ParentData是Widget用来布局子Widget需要的数据就可以了.

为了方便大家理解, 这里举一个比较简单直观的例子: Positioned. Positioned是一个ParentDataWidget, 它的child RenderObjectWidget对应的RenderObject会从它这里拿到我们设置的left、right等位置信息(具体怎么拿到的我们放到下一篇文章里讲). 当Stack对应的RenderObject(叫做RenderStack)开始布局这些RenderObject时, 就可以根据位置信息来确定它们的位置了.

这里要强调的时, 一个RenderObjectWidget最多只能有一个ParentDataWidget做自己的祖先. 即不存在ParentDataWidget -> ParentDataWidget -> RenderObjectWidget的情况.

一张类图总结本文