iOS-Flutter 可滚动组件-可滚动子项缓存

168 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 32 天,点击查看活动详情

可滚动组件子项缓存

之前使用的ListView和GridView组件中,都有一个AddAutomaticKeepAlives属性,,如果AddAutomatocKeepAlive为true时,则ListView会为每一个列表添加一个AutomaticKeepAlive父组件。PageView的默认构造函数和PageView.builder构造函数中没有该参数,但他们最终会生成一个SliverChildDelegate来负责列表项的按需加载,而在SliverChildDelegate中,每当列表项构建完成后,SliverChild Delegate都会为其添加一个AutomaticKeepAlive父组件。

AutomaticKeepAlive

AutommaticKeepAlive的组件主要作用是将列表的根RenderObject的keepAlive按需自动标记为true或false。暂且认为RenderObject对应的组件是列表项的根Widget,代表整个列表项组件,同时我们将列表组件的Viewport区域+cacheExtent(预渲染区域)称为加载区域。

  1. 当keepAlive标记为false时,如果列表项滑出加载区域时,列表组件将会被销毁。
  2. 当keepAlive标记为true时,当列表项滑出加载区域后,Viewport会将列表组件缓存起来,当列表项进入加载区域时,Viewport优先从缓存中查找是否已缓存,如果有则直接复用,如果没有则重新创建列表项。

AutomaticKeepAlive类似于一个Server,它的子组件可以是Client,子组件想改变是否需要缓存的状态时,需要向AutomaticKeepAlive发一个通知消息(KeepAliveNotification),Automatic收到后会去变更keepAlive的状态,如果有必要会同时做一些资源清理工作(比如释放缓存)。

之前提到过,想让PageView页面实现缓存,让Page页变成一个AutomaticKeepAlive Client即可。Flutter中提供了一个AutomaticKeepAlive ClientMixin,我们只需要让PageState混入这个Mixin,同时添加一些必要操作即可。

class _PageState extends State<Page> with AutomaticKeepAliveClientMixin {

  @override
  Widget build(BuildContext context) {
    super.build(context); // 必须调用
    return Center(child: Text("${widget.text}", textScaleFactor: 5));
  }

  @override
  bool get wantKeepAlive => true; // 是否需要缓存
}

提供一个wantKeepAlive参数,表示AutomaticKeepAlive是否需要缓存当前列表项。另外我们必须在build方法中调用一下super.build(context),该方法实现AutomaticKeepAliveClientMixin中,功能就是根据当前的wantKeepAlive的值给AutomaticKeepAlive发送消息,Automatic收到消息后就会对列表项进行标记。 流程图:

image.png 此时运行实例会发现PageView只会创建一次,表明缓存成功了。

注意:如果采用PageView.custom构建页面时,没有给列表包装AutomaticKeepAlive父组件,则上述方案不能正常工作,因为此时Client发出消息后,找不到Server。

KeepAliveWrapper

通过AutomaticKeepAliveClientMixin快速的实现了页面缓存功能,但是通过混入的方式实现不是很优雅。因为必须改Page的代码,有侵入性,使用会不灵活,为此,可以封装一个KeepAliveWrapper,如果那个组件需要缓存,只需要使用KeepAliveWrapper包裹以下即可。

@override
Widget build(BuildContext context) {
  var children = <Widget>[];
  for (int i = 0; i < 6; ++i) {
    //只需要用 KeepAliveWrapper 包装一下即可
    children.add(KeepAliveWrapper(child:Page( text: '$i'));
  }
  return PageView(children: children);
}

实现源码:

class KeepAliveWrapper extends StatefulWidget {
  const KeepAliveWrapper({
    Key? key,
    this.keepAlive = true,
    required this.child,
  }) : super(key: key);
  final bool keepAlive;//控制是否需要缓存
  final Widget child;//传入的widget

  @override
  _KeepAliveWrapperState createState() => _KeepAliveWrapperState();
}

class _KeepAliveWrapperState extends State<KeepAliveWrapper>
    with AutomaticKeepAliveClientMixin {
  @override
  Widget build(BuildContext context) {
    super.build(context);
    return widget.child;//将传入的Widget返回,只是中间加了一些属性,类似于包装。
  }

  @override
  void didUpdateWidget(covariant KeepAliveWrapper oldWidget) {
    if(oldWidget.keepAlive != widget.keepAlive) {
      // keepAlive 状态需要更新,实现在 AutomaticKeepAliveClientMixin 中
      updateKeepAlive();
    }
    super.didUpdateWidget(oldWidget);
  }
//重新系统属性,获取控制变量
  @override
  bool get wantKeepAlive => widget.keepAlive;
}

使用实例:

class KeepAliveTest extends StatelessWidget {
  const KeepAliveTest({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ListView.builder(itemBuilder: (_, index) {
      return KeepAliveWrapper(
        // 为 true 后会缓存所有的列表项,列表项将不会销毁。
        // 为 false 时,列表项滑出预加载区域后将会别销毁。
        // 使用时一定要注意是否必要,因为对所有列表项都缓存的会导致更多的内存消耗
        keepAlive: true,
        child: ListItem(index: index),
      );
    });
  }
}

class ListItem extends StatefulWidget {
  const ListItem({Key? key, required this.index}) : super(key: key);
  final int index;

  @override
  _ListItemState createState() => _ListItemState();
}

class _ListItemState extends State<ListItem> {
  @override
  Widget build(BuildContext context) {
    return ListTile(title: Text('${widget.index}'));
  }

  @override
  void dispose() {
    print('dispose ${widget.index}');
    super.dispose();
  }
}

可以分别设置keepAlive为ture和false看下运行效果,会发现,keepAlive为True时基本无打印,也就是缓存成功,未释放,为false时,则会打印,表示已释放。缓存有利有弊,还是需要看使用场景。开发还是需要本着勤俭节约的原则。