Flutter Widget树、context 基础盘点

462 阅读4分钟

Widget

  • Widget本身是UI描述的配置信息,它并不代表最终绘制在屏幕上的元素。
  • Widget本身的属性就代表的想要渲染元素的某些特征。
  • Widget本身是不可变的,如果要改变Widget的属性,那么只能重新构建一个新的Widget(如果要属性可变,那么只能把属性放在StatefulWidget的State中),所以Widget类中的属性都必须是final不可变的。
  • Widget树只是一张蓝图,并不代表真实的 组件树。
  • Widget的key可以决定在下次build的时候,是否复用旧的Widget对象,通常,key不同,就一定不会复用。类型不同也绝对不会复用。在没有设置key,并且 类型也相同,并且处于相同层级时,下次构建有可能会复用该Widget所对应的Element对象,并有可能保持状态State不变。
  • Widget最核心的一个方法就是:createElement()

Context

上下文。每个Widget对象都有自己的Context,实际上context是当前Widget在Widget树中的操作句柄(handle),它可用来向上查找指定类型的父Widget:context.findAncestorStateOfType<Row>() 像这种写法,就能支持我们查找Widget树中的指定元素。 一个完整的例子,在不利用任何key的情况下打开Scaffold的抽屉drawer。

class GetStateObjectRoute extends StatefulWidget {
  const GetStateObjectRoute({Key? key}) : super(key: key);

  @override
  State<GetStateObjectRoute> createState() => _GetStateObjectRouteState();
}

class _GetStateObjectRouteState extends State<GetStateObjectRoute> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text("子树中获取State对象"),
      ),
      body: Center(
        child: Column(
          children: [
            Builder(builder: (context) {
              return ElevatedButton(
                onPressed: () {
                  // 查找父级最近的Scaffold对应的ScaffoldState对象
                  ScaffoldState _state = context.findAncestorStateOfType<ScaffoldState>()!;
                  // 打开抽屉菜单
                  _state.openDrawer();
                },
                child: Text('打开抽屉菜单1'),
              );
            }),
          ],
        ),
      ),
      drawer: Drawer(),
    );
  }
}

四棵树

Widget只是UI蓝图的配置信息,那么真正绘制在屏幕上的是什么呢?

Widget树->Element树->RenderObject树-> Layer树.

我们通常只会关心前面三棵树:

image.png

  • Widget树通常和 Element树结构的每个节点是一一对应的。比如,一个Column,有对应的ColumnElement类型,Text也有自己对应的TextElement类型。
  • 我们通常自定义组件有两种基本方式,第一,组合现有的Widget,第二,利用RenderObject去自绘。当然也可以利用两种基本方式结合的方式,即复用现有组件,又能按照自己的方式去绘制(Paint,Canvas....)。

Element的生命周期

  • Flutter框架 调用Widget.createElement创建Element实例 element.
  • Flutter框架调用 element.monted方法,创建与element对应的renderOBject对象,然后将该对象attach到 renderObject树中的指定位置,此时,state的mounted这个bool值就变成了 true,表示渲染的元素已经显示在指定位置。
  • 如果State的状态发生变化,build方法会被重新执行,如果build之后产生的 Widget树和之前有所变化,就需要重新构建Element树。为了将Element实例进行复用,Flutter框架会尝试是否可以复用之前相同位置上的Element实例,具体逻辑为:Element会调用其Widget的canUpdate方法,如果此方法返回true,则表示可以复用,旧Element会使用新的Widget配置数据更新,返回false则会创建一个新的Element对象。canUpdate主要是在对比新旧Widget的 runtimeType和key是否相同,同时相等就返回true,否则false。根据这个原理,当我们要强制更新一个Widget的时候,就可以指定不同的key来避免复用。
  • 当Widget树结构发生变化,下面的某些Widget要被移除时,对应的Element树,就会移除该Widget对应的Element对象。然后对应的RenderObject也会被移除。移除之后Element对象会被执行deactive方法被改成inactive状态。
  • 如果Widget树中有一个 Widget要更换位置,其对应的element将会被从element树中原来的位置移除(调用element的deactive方法),再调用(element.activate)将它附加到新的位置上。

BuildContext

  • BuildContext的实例类型其实就是Element,每一个Widget自身的context对象其实就是自身对应的Element。比如,Column,它的context就是ColumnElement。所以,无论是有状态的StatefulWidget或者是无状态的StatelessWidget,他们的build函数都有一个context参数。context可以用来传递上下文携带的参数(比如全局的主题,国际化语言等)。也可以用来查找指定类型的父节点。
  • Element本身,在响应Widget状态变化时,会调用自身的markNeedsBuild把自身标记成dirty状态,随后Element才会rebuild

RenderObject

每一个Element都有自己对应的RenderObject. RenderObject的职责是布局和绘制。如果反推上面两棵树的作用,我是不是可以理解为:

  • Widget树的作用是让开发者通过响应式配置的方式来构建UI
  • Element树的作用则是在Widget树结构发生变化时,尽可能复用已经存在的Element对象(也就是在复用RenderObject对象),减少内存消耗。
  • RenderObject则是纯粹的负责布局绘制上屏渲染UI。