阅读 93

Flutter 100问:为什么PageView切换页面State重新创建

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树如图:

5

可以看到,item Widget上有KeepAlive、AutomaticKeepAlive、_SliverFillViewportRenderObjectWidget。后面会一一介绍其作用。

  • _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

  • KeepAlive

怎样标记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.
  }
}
复制代码
  • AutomaticKeepAlive

实际上有了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

github.com/wslaimin/fl…

Communication

如果对Flutter感兴趣,来Flutter泡泡群冒个泡吧!

QQ: 905105199

文章分类
Android
文章标签