【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分类

上图所列出来的都是直接或者间接继承自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类,继承自DiagnosticableTreeDiagnosticableTree这个类,主要是用于在调试的时候获取子类的各种属性和children信息,这里暂且不管它。

通过源码我们看到它的三个核心部分(属性和方法)

  • Key

    在同一父节点下,用作兄弟节点间的唯一标识,主要用于(结合下面canUpdate方法)控制当 Widget 更新时,对应的 Element 如何处理 (是更新还是新建);

  • Element createElement()

    每个Widget都有一个与之对应的Element,由该方法负责创建,createElement可以理解为设计模式中的工厂方法,具体的Element类型由对应的Widget子类负责创建;

  • bool canUpdate(…)

    canUpdate其实我们在深入理解Key一文中已经见识过了。主要是判断是否可以用 new widget 修改前一帧用 old widget 生成的 Element,而不是创建新的 Element,Widget类的默认实现为:2个WidgetruntimeTypekey都相等时,返回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 层级结构及样式信息。该方法通常只在一下三种情况下调用:

    1. Widget 第一次被加入到 Widget Tree 中 (更准确地说是其对应的 Element 被加入到 Element Tree 时,即 Element 被挂载『mount』时);
    2. Parent Widget修改了其配置信息;
    3. 该 Widget 依赖的Inherited Widget发生变化时。

Parent Widget或 依赖的Inherited Widget频繁变化时,build方法也会频繁被调用。因此,提升build方法的性能就显得十分重要,Flutter 官方给出了几点建议:

  • 减少不必要的中间节点,即减少 UI 的层级,如:对于Single Child Widget,没必要通过组合RowColumnPaddingSizedBox等复杂的 Widget 达到某种布局的目标,或许通过简单的AlignCustomSingleChildLayout即可实现。又或者,为了实现某种复杂精细的 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的。

其生命周期:

State生命周期n

  • 框架通过调用StatefulWidget.createState创建一个State对象 。
  • 新创建的State对象与BuildContext相关联。这种关联是永久性的:State对象永远不会改变它的 BuildContext。但是,BuildContext本身可以与其子树一起在树周围移动。此时State对象被认为是mount
  • StatefulElement 在挂载过程中接着会调用State.initState,子类可以重写该方法执行相关的初始化操作 (此时可以引用contextwidget属性);
  • 同样在挂载过程中会调用State.didChangeDependencies,该方法在 State 依赖的对象 (如:Inherited Widget) 状态发生变化时也会被调用,子类很少需要重写该方法,除非有非常耗时不宜在build中进行的操作,因为在依赖有变化时build方法也会被调用;
  • 此时,State 初始化已完成,其build方法此后可能会被多次调用,在状态变化时 State 可通过setState方法来触发其子树的重建;
  • 此时,element treerenderobject treelayer tree已构建完成,完整的 UI 应该已呈现出来。此后因为变化,element treeparent 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

RenderObjectWidgetRenderObjectlements提供配置信息,通过包装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 ObjectRender Tree上移除时调用该方法。

总结

好了,到了这里Widget介绍总算结束,这里做个总结:

  • Widget本质实际上就是UI的配置信息,本身是immutable的。
  • Widget 从功能上可以分为 3 类:Component WidgetProxy Widget以及Renderer Widget
  • Widget 与 Element 一一对应
  • StatefulWidget 新创建的State对象与BuildContext相关联。这种关联是永久性的:State对象永远不会改变它的 BuildContext。但是,BuildContext本身可以与其子树一起在树周围移动。此时State对象被认为是mount
  • 只有Renderer Widget才会参与最终的 UI 生成过程(Layout、Paint),只有该类型的 Widget 才有与之对应的Render Object,同样由其提供创建方法(createRenderObject)。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇

)">
下一篇>>