当前位置: 代码网 > it编程>App开发>苹果IOS > Flutter之PageView页面缓存与KeepAlive

Flutter之PageView页面缓存与KeepAlive

2024年05月18日 苹果IOS 我要评论
正文如果要实现页面切换和 tab 布局,我们可以使用 pageview 组件。需要注意,pageview 是一个非常重要的组件,因为在移动端开发中很常用,比如大多数 app 都包含 tab 换页效果、

正文

如果要实现页面切换和 tab 布局,我们可以使用 pageview 组件。需要注意,pageview 是一个非常重要的组件,因为在移动端开发中很常用,比如大多数 app 都包含 tab 换页效果、图片轮动以及抖音上下滑页切换视频功能等等,这些都可以通过 pageview 轻松实现。

构造函数

pageview({
  key? key,
  this.scrolldirection = axis.horizontal, // 滑动方向
  this.reverse = false,
  pagecontroller? controller,
  this.physics,
  list<widget> children = const <widget>[],
  this.onpagechanged,
  //每次滑动是否强制切换整个页面,如果为false,则会根据实际的滑动距离显示页面
  this.pagesnapping = true,
  //主要是配合辅助功能用的,后面解释
  this.allowimplicitscrolling = false,
  //后面解释
  this.padends = true,
})

我们看一个 tab 切换的实例,为了突出重点,我们让每个 tab 页都只显示一个数字。

// tab 页面 
class page extends statefulwidget {
  const page({
    key? key,
    required this.text
  }) : super(key: key);
  final string text;
  @override
  _pagestate createstate() => _pagestate();
}
class _pagestate extends state<page> {
  @override
  widget build(buildcontext context) {
    print("build ${widget.text}");
    return center(child: text("${widget.text}", textscalefactor: 5));
  }
}
@override
widget build(buildcontext context) {
  var children = <widget>[];
  // 生成 10 个 tab 页
  for (int i = 0; i < 10; ++i) {
    children.add( page( text: '$i'));
  }
  return pageview(
    // scrolldirection: axis.vertical, // 滑动方向为垂直方向
    children: children,
  );
}

如果将 pageview 的滑动方向指定为垂直方向(上面代码中注释部分),则会变为上下滑动切换页面。

页面缓存

我们在运行上面示例时,可能已经发现:每当页面切换时都会触发新 page 页的 build,比如我们从第一页滑到第二页,然后再滑回第一页时,控制台打印如下:

flutter: build 0
flutter: build 1
flutter: build 0

可见 pageview 默认并没有缓存功能,一旦页面滑出屏幕它就会被销毁, 和listview/gridview 不一样,在创建 listview/gridview 时我们可以手动指定 viewport 之外多大范围内的组件需要预渲染和缓存(通过 cacheextent 指定),只有当组件滑出屏幕后又滑出预渲染区域,组件才会被销毁,但是不幸的是 pageview 并没有 cacheextent 参数!但是在真实的业务场景中,对页面进行缓存是很常见的一个需求,比如一个新闻 app,下面有很多频道页,如果不支持页面缓存,则一旦滑到新的频道旧的频道页就会销毁,滑回去时又得重新请求数据和构建页面,这样极度消耗性能。

按道理 cacheextent 是 viewport 的一个配置属性,且 pageview 也是要构建 viewport 的,那么为什么就不能透传一下这个参数呢?于是笔者带着这个疑问看了一下 pageview 的源码,发现在 pageview 创建viewport 的代码中是这样的:

child: scrollable(
  ...
  viewportbuilder: (buildcontext context, viewportoffset position) {
    return viewport(
      // todo(dnfield): we should provide a way to set cacheextent
      // independent of implicit scrolling:
      // https://github.com/flutter/flutter/issues/45632
      cacheextent: widget.allowimplicitscrolling ? 1.0 : 0.0,
      cacheextentstyle: cacheextentstyle.viewport,
      ...
    );
  },
)

我们发现 虽然 pageview 没有透传 cacheextent,但是却在allowimplicitscrolling 为 true 时设置了预渲染区域,注意,此时的缓存类型为 cacheextentstyle.viewport,则 cacheextent 则表示缓存的长度是几个 viewport 的宽度,cacheextent 为 1.0,则代表前后各缓存一个页面宽度,即前后各一页。既然如此,那我们将 pageview 的 allowimplicitscrolling 置为 true 则不就可以缓存前后两页了?我们修改代码,然后运行示例,发现在第一页时,控制台打印信息如下:

flutter: build 0
flutter: build 1 // 预渲染第二页

当再滑回第一页时,控制台信息不变,这也就意味着第一页缓存成功,它没有被重新构建。但是如果我们从第二页滑到第三页,然后再滑回第一页时,控制台又会输出 ”build 0“,这也符合预期,因为我们之前分析的就是设置 allowimplicitscrolling 置为 true 时就只会缓存前后各一页,所以滑到第三页时,第一页就会销毁。

能缓存前后各一页也貌似比不能缓存好一点,但还是不能彻底解决不了我们的问题。为什么明明就是顺手的事, flutter 就不让开发者指定缓存策略呢?然后我们翻译一下源码中的注释:

todo:我们应该提供一种独立于隐式滚动(implicit scrolling)的设置 cacheextent 的机制。

放开 cacheextent 透传不就是顺手的事么,为什么还要以后再做,是有什么难题么?这就要看看 allowimplicitscrolling 到底是什么了,根据文档以及注释中 issue 的链接,发现pageview 中设置 cacheextent 会和 ios 中 辅助功能有冲突(读者可以先不用关注),所以暂时还没有什么好的办法。看到这可能国内的很多开发者要说我们的 app 不用考虑辅助功能,既然如此,那问题很好解决,将 pageview 的源码拷贝一份,然后透传 cacheextent 即可。 考源码的方式虽然很简单,但毕竟不是正统做法,那有没有更通用的方法吗?有!可滚动组件提供了一种通用的缓存子项的解决方案,答案是有的。

keepalive

aumatickeepalive的组件的主要作用是将列表项的根 renderobject 的 keepalive 按需自动标记 为 true 或 false。为了方便叙述,我们可以认为根 renderobject 对应的组件就是列表项的根 widget,代表整个列表项组件,同时我们将列表组件的 viewport区域 + cacheextent(预渲染区域)称为加载区域 :

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

那么 automatickeepalive 什么时候会将列表项的 keepalive 标记为 true 或 false 呢?答案是开发者说了算!flutter 中实现了一套类似 c/s 的机制,automatickeepalive 就类似一个 server,它的子组件可以是 client,这样子组件想改变是否需要缓存的状态时就向 automatickeepalive 发一个通知消息(keepalivenotification),automatickeepalive 收到消息后会去更改 keepalive 的状态,如果有必要同时做一些资源清理的工作(比如 keepalive 从 true 变为 false 时,要释放缓存)。

我们基于上面 pageview 示例,实现页面缓存,根据上面的描述实现思路就很简单了:让page 页变成一个 automatickeepalive client 即可。为了便于开发者实现,flutter 提供了一个 automatickeepaliveclientmixin ,我们只需要让 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 发送消息,automatickeepalive 收到消息后就会开始工作。

现在我们重新运行一下示例,发现每个 page 页只会 build 一次,缓存成功了。需要注意,如果我们采用 pageview.custom 构建页面时没有给列表项包装 automatickeepalive 父组件,则上述方案不能正常工作,因为此时client 发出消息后,找不到 server,404 了.

keepalivewrapper

虽然我们可以通过 automatickeepaliveclientmixin 快速的实现页面缓存功能,但是通过混入的方式实现不是很优雅,因为必须更改 page 的代码,有侵入性,这就导致不是很灵活,比如一个组件能同时在列表中和列表外使用,为了在列表中缓存它,则我们必须实现两份。为了解决这个问题,笔者封装了一个 keepalivewrapper 组件,如果哪个列表项需要缓存,只需要使用 keepalivewrapper 包裹一下它即可。

@override
widget build(buildcontext context) {
  var children = <widget>[];
  for (int i = 0; i < 10++i) {
    //只需要用 keepalivewrapper 包装一下即可
    children.add(keepalivewrapper(child:page( text: '$i'));
  }
  return pageview(children: children);
}

下面是 keepalivewrapper 的实现源码:

class keepalivewrapper extends statefulwidget {
  const keepalivewrapper({
    key? key,
    this.keepalive = true,
    required this.child,
  }) : super(key: key);
  final bool keepalive;
  final widget child;
  @override
  _keepalivewrapperstate createstate() => _keepalivewrapperstate();
}
class _keepalivewrapperstate extends state<keepalivewrapper>
    with automatickeepaliveclientmixin {
  @override
  widget build(buildcontext context) {
    super.build(context);
    return widget.child;
  }
  @override
  void didupdatewidget(covariant keepalivewrapper oldwidget) {
    if(oldwidget.keepalive != widget.keepalive) {
      // keepalive 状态需要更新,实现在 automatickeepaliveclientmixin 中
      updatekeepalive();
    }
    super.didupdatewidget(oldwidget);
  }
  @override
  bool get wantkeepalive => widget.keepalive;
}

可以看出也是基于automatickeepaliveclientmixin实现了 bool get wantkeepalive => widget.keepalive;并且包裹了子组件。

总结

本章主要介绍了pageview页面缓存的两种方式,automatickeepalive和keepalivewrapper包裹。另外还需要关注viewport区域 + cacheextent的缓存策略和场景。更多关于flutter pageview页面缓存的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com