An instantiation of a [Widget] at a particular location in the tree.
这里我们以Padding
Widget为例,来窥探这三者之间的关系
Padding
是一个Widget集并且继承自SingleChildRenderObjectWidget
,最终的集成关系如下:
Padding -> SingleChildRenderObjectWidget -> RenderObjectWidget -> Widget
我们之前在创建Widget时,经常使用StatelessWidget
和StatefulWidget
,这种Widget只在将其他的Widget在build方法中组织起来,并不是一个真正可以渲染的Widget
在Padding类中,我们找不到任何和渲染相关的代码, 这是因为Padding仅仅作为一个配置信息,这个配置信息会随着我们设置的属性不同,频繁的销毁和创建
那么真正的渲染相关的代码在哪里执行了?
RenderObject
我们来看Padding里面的代码,有一个非常重要的方法:
这个方法其实是来自于RenderObjectWidget的类,在这个类中它是一个抽象方法
这个抽象方法必须被子类实现,但是它的子类SingleChildRenderObjectWidget
也是一个抽象类,所以可以不实现父类的抽象方法
但是Padding不是一个抽象类,必须在这里实现对应的抽象方法,而它的实现就是下面的实现
//Padding的实现
@override
RenderPadding createRenderObject(BuildContext context) {
return RenderPadding(
padding: padding,
textDirection: Directionality.maybeOf(context),
);
}
上面的代码创建了什么了?RenderPadding
RenderPadding -> RenderShiftedBox -> RenderBox -> RenderObject
_padding
和原来保存的value
一致,那么直接return_markNeedResolution
,而_markNeedResolution
内部调用了markNeedsLayout
markNeedsLayout
的目的就是标记在下一帧绘制时,需要重新布局performLayout
我们来思考一个问题:
Element是什么时候创建?
在每一次创建Widget的时候,会创建一个对应的Element,然后将该元素插入树中。
在 SingleChildRenderObjectWidget
中我们可以找到如下代码:
在创建完一个Element之后,Framework会调用mount
方法来将Element插入树中具体的位置
在调用mount
方法时,会使用Widget来创建RenderObject,并且保持对RenderObject的引用
//RenderObjectElement中的mount方法实现
void mount(Element? parent, Object? newSlot) {
super.mount(parent, newSlot);
assert(() {
_debugDoingBuild = true;
return true;
}());
_renderObject = widget.createRenderObject(this); //创建RenderObject对象
assert(() {
_debugDoingBuild = false;
return true;
}());
assert(() {
_debugUpdateRenderObjectOwner();
return true;
}());
assert(_slot == newSlot);
attachRenderObject(newSlot);
_dirty = false;
}
如果你去看Text这种组合类的Widget,它会执行mount
方法,但是mount方法中并没有调用creatRenderObject这样的方法
如果是一个StatefulWidget,则创建出来一个StatefulElement
我们来看下StatefulElement的构造器
StatefulElement(StatefulWidget widget)
: state = widget.createState(),
super(widget) {
...省略代码
state._widget = widget;
assert(state._debugLifecycleState == _StateLifecycle.created);
}
而调用build
方法的时候,本质上是调用_state
中的build方法:
Widget build() => state.build(this);
在StatelessElement中,我们发现将this传入,所以本质上BuildContext就是当前的Element:Widget build() => state.build(this);
我们来看下继承关系图:
abstract class Element extends DiagnosticableTree implements BuildContext
在StatefulElement中,build方法也是类似,调用state的build方式时,传入的是this(Widget build() => state.build(this);
)
Widget只是配置了描述信息:
createElement
方法用于创建Element
createRenderObject
,但是不是自己在调用Element是真正保存树结构的对象:
RenderObject是真正渲染的对象:
markNeedsLayout
performLayout
markNeedsPaint
paint
等方法在我们创建Widget的时候,总是会看到一个key的参数,它又是做什么的呢?
class GYHomePage extends StatefulWidget {
const GYHomePage({
Key? key}) : super(key: key);
@override
_GYHomePageState createState() => _GYHomePageState();
}
class _GYHomePageState extends State<GYHomePage> {
List<String> items = ["aaaaa","bbbbb","ccccc"];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("test key"),
),
body: ListView(
children: items.map((name) {
return GYHomeItemless(name);
}).toList(),
),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.delete),
onPressed: (){
setState(() {
items.removeAt(0);
});
},
),
);
}
}
class GYHomeItemless extends StatelessWidget {
final String name;
final randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));
GYHomeItemless(this.name);
@override
Widget build(BuildContext context) {
return Container(
height: 80,
color: randomColor,
child: Text(name, style: TextStyle(color: Colors.white, fontSize: 30),),
);
}
}
由上图可知,它是没删除一个,其它的颜色都会发生一次变化
setState
方法,就会重新调用build方法,那么重新build出来的StatelessWidget会重新生成一个颜色class GYHomeitemful extends StatefulWidget {
final String name;
GYHomeitemful(this.name);
@override
_GYHomeitemfulState createState() => _GYHomeitemfulState();
}
class _GYHomeitemfulState extends State<GYHomeitemful> {
final randomColor = Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256));
@override
Widget build(BuildContext context) {
return Container(
height: 80,
color: randomColor,
child: Text(this.widget.name, style: TextStyle(color: Colors.white, fontSize: 30),),
);
}
}
我们可以发现一个很奇怪的现象,颜色没有改变,但是数据像上移动了
该方法是判断该Element需不需要重建,这里比较两个widget(runtimeType和key),当返回true的时候,就表示是完全一样的widget,那么就不会创建新的Element
这里调用setState方法之后, 就会重新生成一个新的widget tree
, 饭后比较新旧widget, 发现有两个Element对象是完全可以用的不用重建,所以就直接使用, 那么该Element中保存的state的一些信息 就直接展示, 最后一个发现没有widget比较所以直接删除,这就是为什么颜色没有改变,数据像上移动的原因
没有重建的Element会保存以前的state对象的信息, 然后重新和新生成的widget对象,关联上
GYHomeitemful代码修改如下:
class GYHomeitemful extends StatefulWidget {
final String name;
GYHomeitemful(this.name,{
Key? key}) : super(key: key);
@override
_GYHomeitemfulState createState() => _GYHomeitemfulState();
}
home代码修改
body: ListView(
children: items.map((name) {
//UniqueKey(), flutter会自动帮我们创建一个唯一的key
//这里也可以使用随机数,但是使用随机数也有可能重复
return GYHomeitemful(name, key: UniqueKey(),);
}).toList(),
),
我们可以发现,当我们设置了key属性之后,当我们删除时每次的widget都出现了随机颜色的现象,这是因为修改了key以后,在比较widget的时候肯定会不通过,那么就会重新创建Element,Element会强制刷新,那么对应的state也会重建
body: ListView(
children: items.map((name) {
return GYHomeitemful(name, key: ValueKey(name),);
}).toList(),
),
这次实现了我们理想的效果:
key
进行diff算法key本身是一个抽象类,不过它有一个工程构造器,创建出来一个ValueKey,直接的子类有:LocalKey
和GlobalKey
LocalKey
:它应用于具有相同父Element的Widget进行比较,也是diff算法的核心所在GlobalKey
,通常我们会使用GlobalKey某个Widget对应的Widget或State或ElementLocalKey有三个子类
ValueKey
:当我们使用特定的值作为key时,比如一个字符串,数字等等ObjectKey
:在某些时候,单存的字符串,等属性不能确定唯一的时候,这个时候我们可以创建出一个对象来作为keyUniqueKey
:比如要确保唯一性,就可以使用UniqueKey
对象GlobalKey可以帮助我们访问某个Widget的信息,包括Widget或State或Element等对象
下面例子,我们可以在GYHomePage中访问GYHomeContent中的属性
class GYHomePage extends StatelessWidget {
//创建一个GlobalKey
final GlobalKey<_GYHomeContentState> _globalKey = GlobalKey();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("test key"),
),
body: GYHomeContent(key: _globalKey,),
floatingActionButton: FloatingActionButton(
child: Icon(Icons.delete),
onPressed: (){
print("获取绑定key的widget属性:${_globalKey.currentState?.widget.name}");
print("获取绑定key的widget属性:${_globalKey.currentState?.value}");
},
),
);
}
}
class GYHomeContent extends StatefulWidget {
final String name = "test_globalKey";
const GYHomeContent({
Key? key}) : super(key: key);
@override
_GYHomeContentState createState() => _GYHomeContentState();
}
class _GYHomeContentState extends State<GYHomeContent> {
final String value = "test_value";
@override
Widget build(BuildContext context) {
return Container();
}
}