Why
PageView的渲染方式为Sliver模型(另一种是Box模型),这种模型针对的是页面懒加载,比如列表视图在视窗显示范围内的显示,反之不显示。
所以,划出PageView显示区域的Widge、Element、RenderObject默认情况是会被销毁的。这也是State被反复重新创建的原因。
How
Sliver模型提供了一种缓存State的方法:
- 继承AutomaticKeepAliveClientMixin
- wantKeepAlive返回true
- 重写build()方法并调用父类build()方法
class _ContentViewState2 extends State<_ContentView> with AutomaticKeepAliveClientMixin{
@override
Widget build(BuildContext context) {
super.build(context);
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Num:$_count'),
ElevatedButton(
child: Text('increase'),
onPressed: () {
_count++;
setState(() {});
},
)
],
);
}
@override
bool get wantKeepAlive => true;
}
More
知其然也要知其所以然。AutomaticKeepAliveClientMixin,是如何起作用的呢?
PageView以下大致Widget树如图:

可以看到,item Widget上有KeepAlive、AutomaticKeepAlive、_SliverFillViewportRenderObjectWidget。后面会一一介绍其作用。
_SliverFillViewportRenderObjectWidget的父类为SliverMultiBoxAdaptorWidget,可以看做Sliver模型中能够容纳多个child的容器。
在item划出显示区域,_SliverFillViewportRenderObjectWidget的RenderSliver(RenderSliverFillViewport实例)对象,销毁item RenderObject。
void _destroyOrCacheChild(RenderBox child) {
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
if (childParentData.keepAlive) {
assert(!childParentData._keptAlive);
remove(child);
_keepAliveBucket[childParentData.index!] = child;
child.parentData = childParentData;
super.adoptChild(child);
childParentData._keptAlive = true;
} else {
assert(child.parent == this);
_childManager.removeChild(child);
assert(child.parent == null);
}
}
从上面可以清楚看到,被标记为keepAlive的RenderObject将会被缓存。
即将显示的item RenderObject会被创建。
void _createOrObtainChild(int index, { required RenderBox? after }) {
invokeLayoutCallback<SliverConstraints>((SliverConstraints constraints) {
assert(constraints == this.constraints);
if (_keepAliveBucket.containsKey(index)) {
final RenderBox child = _keepAliveBucket.remove(index)!;
final SliverMultiBoxAdaptorParentData childParentData = child.parentData! as SliverMultiBoxAdaptorParentData;
assert(childParentData._keptAlive);
dropChild(child);
child.parentData = childParentData;
insert(child, after: after);
childParentData._keptAlive = false;
} else {
_childManager.createChild(index, after: after);
}
});
}
创建过程优先从缓存中取RenderObject
怎样标记RenderObject为keepAlive,KeepAlive Widget就是做这个工作的。
KeepAlive的父类是ParentDataWidget<KeepAliveParentDataMixin>
。对应的Element是ParentDataElement。
RenderObject在attach期间向上查找ParentDataElement对象,并将上层数据赋值给RenderObject的parentData属性。
@override
void applyParentData(RenderObject renderObject) {
assert(renderObject.parentData is KeepAliveParentDataMixin);
final KeepAliveParentDataMixin parentData = renderObject.parentData! as KeepAliveParentDataMixin;
if (parentData.keepAlive != keepAlive) {
parentData.keepAlive = keepAlive;
final AbstractNode? targetParent = renderObject.parent;
if (targetParent is RenderObject && !keepAlive)
targetParent.markNeedsLayout(); // No need to redo layout if it became true.
}
}
实际上有了KeepAlive就已经能够标记缓存了,但是这是一种自上而下的方式,并且PageView隐藏了KeepAlive,不能让item Widget自行决定要不要缓存。
有了AutomaticKeepAlive和AutomaticKeepAliveClientMixin就可以让要不要缓存由item Widget自行决定。
AutomaticKeepAliveClientMixin的wantKeepAlive为true,在initeState()和build()期间就向上发送一个notification。
void _ensureKeepAlive() {
assert(_keepAliveHandle == null);
_keepAliveHandle = KeepAliveHandle();
KeepAliveNotification(_keepAliveHandle!).dispatch(context);
}
AutomaticKeepAlive收到notification,重新构建Widget树,并把KeepAlive的keepAlive属性设置为true。这样就可以重新标记item Widget为keepAlive了。
Demo
https://github.com/wslaimin/flutter_hundred_questions/tree/master/lib/about/pageview
Communication
如果对Flutter感兴趣,来Flutter泡泡群冒个泡吧!
QQ: 905105199