三羊

三羊的小站

flutter 学习小结(二)

November 26, 2019/「 flutter / Edit on Github ✏️

上一篇文章中,学习了 flutter 跨平台的特性,以及 dart 的包管理器 pub。这一篇,我们将详细了解 flutter 很重要的一个概念,widget。

组件

flutter 是响应式的,它通过编程方式去写 UI 页面,而不是像 html 或者 android 中的 xml 等模版语言去定义 UI。它借鉴了 React,也是以组件的方式去定义一个一个 UI 部件。

Flutter widgets are built using a modern framework that takes inspiration from React.

分类

在 flutter 中一切皆是 widget。按照状态划分,可以分为 StatelessWidget 和 StatefulWidget,与 React 中的 Stateful Class Component 和 Stateless Function Component 比较类似;按照功能划分,又有 layout widget,style widget,content widget;按照 child 接受单个 widget,或者多个 widget,又有 SingleChildRenderObjectWidget 和 MultiChildRenderObjectWidget。常见 widget 如下,

  1. layout widget,例如ScaffoldRowColumnContainer等;
  2. style widget,例如ContainerPaddingCenter等;
  3. content widget,例如TextImageRaisedButton等;

在开发 flutter 页面时,可以通过组合多个 widget,实现一个复杂或者独立功能的自定义 widget。对于描述一个 widget 的样式时,需要使用样式 widget 进行包装,或者更改 widget 提供的某些样式属性,比如Containerpadding属性等。

实现

在开发自定义 widget 时,一般都是继承自 StatelessWidget,或者 StatefulWidget。StatelessWidget 和 StatefulWidget 又都是继承自 Widget。下面我们通过源码来看看它们的实现。

/// 列出主要实现,去掉了其他不重要的属性或者方法

abstract class Widget extends DiagnosticableTree {
  const Widget({ this.key });

  final Key key;

  
  Element createElement();

  static bool canUpdate(Widget oldWidget, Widget newWidget) {
    return oldWidget.runtimeType == newWidget.runtimeType
      && oldWidget.key == newWidget.key;
  }
}

Widget 是一个抽象类,主要有一个createElement的抽象方法和canUpdate的静态方法。createElement需要由继承它的子类来实现,必须返回一个 Element 对象,每一个 widget 都会有一个关联的 element 对象;在 Widget 上面有@immutable声明,描述 widget 对象是不可变的,它的属性都应该使用final标记,每次做 rebuild 时,都只会重新创建新的 widget 对象。widget 是不可变的,但是 element 是可变的,可以把 widget 看成是 element 的静态配置信息,最终是用 element 信息来完成渲染的。为了尽可能复用 element 对象,每次更新时都会先调用canUpdate,通过runtimeTypekey来比较新旧 widget,如果返回 true,则复用旧的 element,并更新 element 的 widget 为新的 widget,否则调用新的 widget 的createElement来生成新的 element。

abstract class StatelessWidget extends Widget {
  const StatelessWidget({ Key key }) : super(key: key);

  
  StatelessElement createElement() => StatelessElement(this);

  
  Widget build(BuildContext context);
}

对于 StatelessWidget 的实现,它继承 Widget,重写了createElement用于生成一个StatelessElement。它还声明了一个build方法,接受一个BuildContext参数,并返回一个 Widget。实际上,这个BuildContext就是上面通过createElement创建的 Element 对象,下面将会解释到。

abstract class StatefulWidget extends Widget {
  const StatefulWidget({ Key key }) : super(key: key);

  
  StatefulElement createElement() => StatefulElement(this);

  
  State createState();
}

对于 StatefulWidget,就复杂一些了。它跟 StatelessWidget 一样,继承 Widget,提供一个createElement方法,但是返回是StatefulElement对象;跟 StatelessWidget 不一样的是,它没有声明build方法,而是有一个createState方法,生成一个 State 对象。前面说过,Widget 是不可变的,每次 rebuild 都会销毁和重建,所以状态不能直接放在 widget 中,而是需要独立出来,关联到 element 上,这样状态才不会丢失。

abstract class RenderObjectWidget extends Widget {
  const RenderObjectWidget({ Key key }) : super(key: key);

  
  RenderObjectElement createElement();

  
  RenderObject createRenderObject(BuildContext context);

  
  void updateRenderObject(BuildContext context, covariant RenderObject renderObject) { }

  
  void didUnmountRenderObject(covariant RenderObject renderObject) { }
}

RenderObjectWidget,是比较特殊的一类 widget。它声明了createRenderObject去创建一个 RenderObject,而在 flutter 中真正被渲染的对象就是 RenderObject。上面提到的 StatelessWidget,StatefulWidget,它们都没有直接提供 RenderObject 的实现,而是通过组合其他 RenderObjectWidget,从而实现渲染出某一个部分组合 UI。根据接受的参数个数,它有两个子类,

  1. SingleChildRenderObjectWidget,仅接受一个 child,比如 Padding;

  2. MultiChildRenderObjectWidget,可以接受多个 child,比如 Row;

State

StatefulWidget 会通过createState生成一个 State 对象,它会被关联到对应的 StatefulElement 上。State 不会随着 widget 的销毁而销毁,而是会保持其生命期跟随着 element。一个 State 对象,在整个生命周期内,会有不同的阶段,为了方便开发者针对不同阶段处理一些事情,它会暴露出相应的钩子函数,类似生命周期方法。


abstract class State<T extends StatefulWidget> extends Diagnosticable {
  T get widget => _widget;
  T _widget;

  _StateLifecycle _debugLifecycleState = _StateLifecycle.created;

  BuildContext get context => _element;
  StatefulElement _element;

  bool get mounted => _element != null;

  
  
  void initState() {}

  
  
  void didUpdateWidget(covariant T oldWidget) { }

  
  void setState(VoidCallback fn) {
    final dynamic result = fn() as dynamic;
    _element.markNeedsBuild();
  }

  
  
  void deactivate() { }

  
  
  void dispose() {}

  
  Widget build(BuildContext context);

  
  
  void didChangeDependencies() { }
}

State 对象会持有 StatefulWidget 的引用_widget,还有一个_StateLifecycle 状态。在不同的阶段,会调用不同的方法,然后_StateLifecycle 的值也会不一样,类似 react 中的生命周期方法。这里主要有如下生命周期方法,

  1. initState,初始化时执行,一般用于执行申请资源等操作,不可以执行setState,在 state 的生命里只会调用一次。
  2. didChangeDependencies,如果在build中有调用BuildContext.inheritFromWidgetOfExactType,而且 inherited widgets 有变化,则会触发此方法。可以根据变化,调用setState来更新当前 children widgets。
  3. didUpdateWidget,当 element 上调用的canUpdate返回 true 时,也就是复用 element,更新 element 关联的 widget 时会触发此方法。state 需要根据新的 widget 来作出改变。在执行完didUpdateWidget时,框架紧接着会执行 statet 的build方法来更新 UI,所以不应该在此方法中执行setState
  4. deactivate,当 element 上调用的canUpdate返回 false 时,会卸载当前的 element,随即也会卸载当前的 element 关联的 state,则会触发此方法。State 会被从树中移除,应该在此方法里解除它与其他 element 的引用关系。
  5. dispose,销毁时执行,一般用于释放在initState中申请的资源,不可以执行setState

State 会有一个 context 的属性,它实际就是 widget 的 element 对象。State 有一个比较重要的方法setState,每次调用它,都会触发当前 widget 的更新。在上面的实现中,可以看到它调用了_element.markNeedsBuild(),将当前 element 标记为 dirty,然后在下一个 frame 中会重新执行 rebuild。如果直接更改 state 中的值,是不会触发 widget 的更新的。

Element Tree

通过上面的部分源码分析,真正负责渲染的 widget 对象是 RenderObjectWidget,它会生成 Element 和 RenderObject,关系如下

internals_element

把它们与 React 中元素来做类似关联,应该就比较明白它们的作用了,

  1. widget,类似于 React 中 JSX,用于定义 UI 应该展示什么样子,在最上层。
  2. element,类似于 React 中 Component 生成的 vdom 对象,这里会做一些复用逻辑和渲染优化,用于为真正渲染对象提供数据,位于中间层。
  3. renderObject,类似于真正的 DOM 了,真正的渲染对象,在最底层了。

react 会形成一颗虚拟的 dom 树,flutter 中也在 element 这一层生成一颗 element 树。

flutter 框架会帮我们处理好 element 和 renderObject 层,对于一般业务开发,我们只需要在 widget 层去实现功能即可。框架会在三个地方帮我们调用createElement去生成对应的 element 对象,下面我们分别来看看这三个地方。

第一个地方就是生成 element 树的根节点。flutter 是通过调用runApp来启动的,如下

void main() => runApp(MyApp());

runApp中会做很多初始化的工作,其中就包含了生成 element 树的根节点并挂载。

void runApp(Widget app) {
  WidgetsFlutterBinding.ensureInitialized()
    /// 这里就会生成element树的根节点
    ..scheduleAttachRootWidget(app)
    ..scheduleWarmUpFrame();
}

沿着scheduleAttachRootWidget调用栈一路找下去,最后会生成一个RenderObjectToWidgetAdapter对象,并调用它的attachToRenderTree方法,

 class RenderObjectToWidgetAdapter<T extends RenderObject> extends RenderObjectWidget {

  
  RenderObjectToWidgetElement<T> createElement() => RenderObjectToWidgetElement<T>(this);

  RenderObjectToWidgetElement<T> attachToRenderTree(BuildOwner owner, [ RenderObjectToWidgetElement<T> element ]) {
    if (element == null) {
      owner.lockState(() {
        // 如果root element为空,则调用createElement生成root element
        element = createElement();
        assert(element != null);
        element.assignOwner(owner);
      });
      owner.buildScope(element, () {
        // 并挂载在element树上
        element.mount(null, null);
      });
      // This is most likely the first time the framework is ready to produce
      // a frame. Ensure that we are asked for one.
      SchedulerBinding.instance.ensureVisualUpdate();
    } else {
      element._newWidget = this;
      element.markNeedsBuild();
    }
    return element;
  }
}

先判断当前 root element 存不存在,如果不存在,就会调用createElement生成新的 root element,并调用element.mount(null, null)进行挂载

第二个地方就是进行 mount 时,如果当前 element 是 MultiChildRenderObjectElement,则会为它的 widget 的 children widget 生成对应的 element,并挂载。

  void mount(Element parent, dynamic newSlot) {
    super.mount(parent, newSlot);
    _children = List<Element>(widget.children.length);
    Element previousChild;
    for (int i = 0; i < _children.length; i += 1) {
      /// inflateWidget就是调用createElement并进行mount
      final Element newChild = inflateWidget(widget.children[i], previousChild);
      _children[i] = newChild;
      previousChild = newChild;
    }
  }
  Element inflateWidget(Widget newWidget, dynamic newSlot) {
    assert(newWidget != null);
    final Key key = newWidget.key;
    if (key is GlobalKey) {
      /// ...
    }
    final Element newChild = newWidget.createElement();
    newChild.mount(this, newSlot);
    return newChild;
  }

第三个地方就是进行 rebuild 时,更新 child element,如果 child element 为 null,且新的 widget 不为 null,则也会调用inflateWidget来新建 element 且挂载。

Element updateChild(Element child, Widget newWidget, dynamic newSlot) {
    // newWidget为null,则直接返回null
   if (newWidget == null) {
      /// ...
      return null;
    }
   //child不为null,且newWidget也不为null
    if (child != null) {
      // 如果widget相等,则直接用之前的element
      if (child.widget == newWidget) {
        /// ...
        return child;
      }
      // 如果canUpdate返回true,也直接用之前的element
      if (Widget.canUpdate(child.widget, newWidget)) {
        /// ...
        child.update(newWidget);
        return child;
      }
    }
   // child element为null,且newWidget不为null,则生成新的child element
    return inflateWidget(newWidget, newSlot);
  }

在维护更新 element 树的同时,会每次将更新点同步到 RenderObject 上。最后,flutter 根据 RenderObject 来渲染出页面。为了更高效的渲染,在 layout 阶段会通过 RelayoutBoundary 来控制 relayout 的范围,在 paint 阶段会通过 RepaintBoundary 只重新绘制受响应的部分。

结构图

从 Widget,Element,RenderObject 角度分别列举了它们的父类和子类,

structure

注意点

  1. flutter 渲染分为三层,框架会自动处理好 Element 层和 RenderObject 层,大部分时候只需要编写 Widget 层即可。
  2. Widget 是不可变的,总应该将它的属性标记为final
  3. 对于无状态的 Widget,总是应该使用 StatelessWidget,避免额外的创建和维护 State。
  4. 在更新 element 时,会直接判断 new widget 与 old widget 是否相等,如果相等则直接复用旧的 element。所以我们可以通过 state 来提前保存生成好的子 widget,而不必每次都在 build 中生成新的子 widget。
  5. 对于在整个渲染树中复用的 element,可以给它对应的 widget 增加一个 global key。这样在更新时,会通过Widget.canUpdate来判断是否可以复用 element。
  6. RenderObject 并没有定义节点的布局模型,它有 2 个子类,一个是 RenderBox,定义了节点在笛卡尔坐标系中的布局方式,一个是 RenderSliver,定义了节点在滚动时的布局方式。

小结

我们大部分时候都是直接与 Widget 在打交道,而很少关心底层的 Element 和 RenderObject。Widget 在 flutter 中承担了非常多的功能,而不仅仅只是组合 UI,例如 flutter 中的路由管理 Navigator 就是 StatefulWidget,样式主题 Theme 是 StatelessWidget 等。flutter 提供了非常丰富的 Widget,有 Material Design 风格的 material widget,有 IOS 风格的 cupertino widget。

参考

若有收获,小额鼓励