由一个bug引发的flutter的widget跟element关系的源码分析
在页面本来有照片数据的(第一张照片数据),点击加号唤起系统拍照功能后,再返回页面A,原来的照片数据丢失了(部分Android机型上必现)
照片跟UI是一个statefulWidget,照片数据是放在widget的class下面的,在调起拍照后,返回app,系统触发了widget的build,widget被重新创建了,所以保留的List数组数就没了
class TestWidget extends StatefulWidget {
// 这个数组,用来保存照片信息
// widget在每次build后会重新创建,之前photoList的数据就丢失了
List<String> photoList = <String>[];
@override
State<TestWidget> createState() => _TestWidgetState();
@override
StatefulElement createElement() {
return TestElement(this);
}
}
修复方法也很简单,把数组挪到state的class里面就好了,虽然widget会重新创建,但是state还是原来的state,不会重新创建
class _TestWidgetState extends State<TestWidget> {
// 数据放在State里面,而不是widget里面
List<String> photoList = <String>[];
}
虽然bug当时就修复了,但是为什么系统的表现是这样,还是要去查看源码
为了方便分析问题,创建一个自定义的statefulWidget跟StatefulElement
class _TestWidgetState extends State<TestWidget> {
_TestWidgetState() {
log('_TestWidgetState created');
}
@override
Widget build(BuildContext context) {
return Container(
width: 100,
height: 100,
color: Colors.red,
);
}
}
class TestElement extends StatefulElement {
TestElement(StatefulWidget widget) : super(widget);
@override
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
log('updateChild child $child newWidget $newWidget newSlot $newSlot ');
return super.updateChild(child, newWidget, newSlot);
}
}
每次执行到对应的系统方法,可以打印log,也方便调试源码;然后把这个widget添加到布局中,验证widget在布局的第一次加载和后续的更新中的element的表现
widget首次加载在页面启动,widget首次加载的log如下,先是新建了widget,然后新建了element,又新建了state
[log] TestWidget create
[log] createElement
[log] _TestWidgetState created
widget的更新
然后是页面调用setState,触发页面的刷新,log可以发现,widget被重新创建,而element跟state都没有重新创建
[log] TestWidget create
const的widget在这个demo的widget前面加上const,代表是不变的,在每次调用setState刷新的情况下,widget不会被重新创建了
return Column(
children: [
const TestWidget()
],
);
接下来,从源码角度分析下上述行为
widget树的目的,是为了生成element树,核心的方法就是在updateChild
这里生成,源码如下,相关的代码我直接加上了注释,删掉一些无关的代码
Element? updateChild(Element? child, Widget? newWidget, Object? newSlot) {
// 如果对应的widget是空的,则直接把element移除
// deactivateChild会把child这个element移除
if (newWidget == null) {
if (child != null)
deactivateChild(child);
return null;
}
final Element newChild;
if (child != null) {
bool hasSameSuperclass = true;
if (hasSameSuperclass && child.widget == newWidget) {
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
newChild = child;
} else if (hasSameSuperclass && Widget.canUpdate(child.widget, newWidget)) {
// 后续的刷新,其实是走到这里,widget重新创建了
// 创建后的widget跟旧的wiget是同个类型的widget,于是只是更新了element对应的widget就好,不会重新创建element
if (child.slot != newSlot)
updateSlotForChild(child, newSlot);
child.update(newWidget);
newChild = child;
} else {
deactivateChild(child);
newChild = inflateWidget(newWidget, newSlot);
}
} else {
// 第一次加载,其实是走到这里,widget被创建了,但是没有对应的element
// inflateWidget会生成这个widget对应的element
newChild = inflateWidget(newWidget, newSlot);
}
return newChild;
}
当父element创建好,就会调用updateChild
去更新它的child的element,然后就会触发上面的方法,包括每次刷新,也是widget被重新创建的,不过只有两种场景下才会重新创建element
第二种,判断widget有没有变的源码如下
static bool canUpdate(Widget oldWidget, Widget newWidget) {
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
}
会判断widget的类型和key有没有变,大多数情况下,key都是null,所以类型没变,element就也不会变
至于state的创建,其实是跟着element一起创建的
class StatefulElement extends ComponentElement {
StatefulElement(StatefulWidget widget)
: _state = widget.createState(),
super(widget) {
element创建的初始化方法中,会创建对应的state