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树.
我们通常只会关心前面三棵树:
- 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。