Flutter之PageView页面缓存与KeepAlive

目录
  • 正文
  • 构造函数
  • 页面缓存
  • KeepAlive
  • KeepAliveWrapper
  • 总结

正文

如果要实现页面切换和 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)

相关推荐

  • Flutter之TabBarView组件项目实战示例

    目录 TabBarView TabBar TabBarView+项目实战 1 构建导航头部搜索框 2 构建导航头部TabBar 3 构建导航底部TabBarView容器 4 构建导航底部结构填充 5 构建导航底部结构轮播图 6 构建导航底部结构信息流 总结 TabBarView TabBarView 是 Material 组件库中提供了 Tab 布局组件,通常和 TabBar 配合使用. TabBarView 封装了 PageView,它的构造方法: TabBarView({ Key? key,

  • Flutter开发Widgets 之 PageView使用示例

    目录 构造方法以及参数: 基本用法 无限滚动 实现指示器 切换动画 总结: 构造方法以及参数: PageView可用于Widget的整屏滑动切换,如当代常用的短视频APP中的上下滑动切换的功能,也可用于横向页面的切换,如APP第一次安装时的引导页面,也可用于开发轮播图功能. PageView({ Key? key, this.scrollDirection = Axis.horizontal, // 设置滚动方向 垂直 / 水平 this.reverse = false, // 反向滚动 Pag

  • Android开发之Flutter与webview通信桥梁实现

    前言 最近业务有需求需要在flutter内使用webview进行内嵌H5进行展示,此时需要涉及到H5与flutter之间额通信问题.比如发送消息或者H5调用Flutter的相机等等 webview选型 这里我们使用官方维护的插件webview_flutter 如何通信? webview在初始化的时候需要向容器内注册一个全局方法供H5进行调用 WebView( initialUrl: 'https://flutter.dev', javascriptMode: JavascriptMode.unr

  • 使用PlatformView将 Android 控件view制作成Flutter插件

    目录 引言 1. FlutterPlugin 创建 2. 创建 Android 控件 3. 注册 Android 控件 4. 封装 Android 层通信交互 ‘CustomViewController’ 代码说明 5. 在 flutter 中如何使用已注册的 Android 控件(view) 代码说明 如何使用这个View 6. 附上 example 完整代码 引言 小编最近在项目中实现相机识别人脸的功能,将 Android 封装的控件 view 进行中转,制作成 FlutterPlugin

  • Flutter之 ListView组件使用示例详解

    目录 ListView的默认构造函数定义 默认构造函数 ListView.builder ListView.separated 固定高度列表 ListView 原理 实例:无限加载列表 添加固定列表头 总结 ListView的默认构造函数定义 ListView是最常用的可滚动组件之一,它可以沿一个方向线性排布所有子组件,并且它也支持列表项懒加载(在需要时才会创建).我们看看ListView的默认构造函数定义: ListView({ ... //可滚动widget公共参数 Axis scrollD

  • Flutter之PageView页面缓存与KeepAlive

    目录 正文 构造函数 页面缓存 KeepAlive KeepAliveWrapper 总结 正文 如果要实现页面切换和 Tab 布局,我们可以使用 PageView 组件.需要注意,PageView 是一个非常重要的组件,因为在移动端开发中很常用,比如大多数 App 都包含 Tab 换页效果.图片轮动以及抖音上下滑页切换视频功能等等,这些都可以通过 PageView 轻松实现. 构造函数 PageView({ Key? key, this.scrollDirection = Axis.horiz

  • flutter实现切换页面缓存

    本文实例为大家分享了flutter实现切换页面缓存的具体代码,供大家参考,具体内容如下 一.实现底部导航栏切换页面缓存 实现底部导航栏切换页面缓存需要在pubspc.yamal中导入proste_indexed_stack插件,这个插件可以实现懒加载,比起使用IndexedStack包裹body实现,性能更好. dependencies: #懒加载的层叠组件 proste_indexed_stack:  //不加版本号可获取最新版本 实现底部导航切换页面缓存只需将body用ProsteIndex

  • Vue实现前端页面缓存的过程

    目录 Vue前端页面缓存 1.使用情景 2.keep-alive基本认识 3.keep-alive实现路由页面缓存 vue页面缓存问题 1.路由设置 2.页面路由嵌套设置 3.从指定页面返回使用缓存数据 Vue前端页面缓存 1.使用情景 在使用Vue开发单页面应用时,我们通常会使用Vue-Router进行页面导航,Vue-Router在进行路由切换的时候,页面是会重新加载,对应的生命周期函数也会再次执行一遍,但是在有些业务场景下, 比如: 在有分页数据列表中,切换到第三页需要查看列表对应数据的详

  • vue缓存的keepalive页面刷新数据的方法

    用到这个的业务场景是这样的: a页面点击新建列表按钮进入到新建的页面b,填写b页面并点击b页面确认添加按钮,把这些数据带到a页面,填充到列表(数组),可以添加多条, 点击这条的时候进入到编辑页面,确认修改之后,回退到a页面,a页面需要更新这条数据 实现这个功能的时候,由于是路由页面之间的跳转,首先想到的方案有几个:1. 用sessionStorage本地存储:2. 用路由参数带过去:3. 用兄弟组件传值 由于是添加完之后如果按回退是需要退出整个页面,如果用路由跳转,会出现回退到编辑页面了,所以这

  • vuex + keep-alive实现tab标签页面缓存功能

    在开发很多管理系统过程之中,常遇到这种需求,需要对打开路由页面进行缓存,然后在系统页眉提供方便查阅的tab标签进行切换以及对已经缓存页面进行数据刷新和清除数据操作.具体演示如下图所示: 在上面演示中实现了类似 window tab 标签页效果,会对当前数据进行缓存.在浏览器中实现对路由页面的缓存可以减少接口请求,也方便了用户来回切换想搜索的数据列表. 原理 Vue 提供的 keep-alive API实现对路由组件的缓存. include 属性可以绑定一个数组,里面是需要路由组件的 name 值

  • 详解基于vue的移动web app页面缓存解决方案

    现在移动web app越来越热门了,许多公司开始尝试使用angular.react.vue等MVVM框架来开发单页架构的web app.但在开发web app时,如果希望页面的导航体验也接近原生应用,那一般都会遇到这两个问题: 识别前进后退行为 后退时恢复之前的页面 笔者开发了一个基于vue与vue-router的导航库vue-navigation,来帮助开发者来解决这些问题,下面是问题的解决思路. 识别前进后退 先说第一个问题.和原生app不一样,浏览器中主要有这几个限制: 没有提供前进后退的

  • 详解Nginx 反向代理、负载均衡、页面缓存、URL重写及读写分离详解

    注,操作系统为 CentOS 6.4 x86_64 , Nginx 是版本是最新版的1.4.2,所以实验用到的软件请点击这里下载: CentOS 6.4下载地址:http://www.jb51.net/softs/78243.html Nginx下载地址:http://www.jb51.net/softs/35633.html 一.前言 在前面的几篇博文中我们主要讲解了Nginx作为Web服务器知识点,主要的知识点有nginx的理论详解.nginx作为web服务器的操作讲解.nginx作为LNM

  • 详解vue服务端渲染浏览器端缓存(keep-alive)

    在使用服务器端渲染时,除了服务端的接口缓存.页面缓存.组建缓存等,浏览器端也避免不了要使用缓存,减少页面的重绘. 这时候我们就会想到vue的keep-alive,接下来我们说一下keep-alive的使用 假如现在我们有两个页面,home.vue 和 about.vue home.vue <template> <div> home </div> </template> <script> export default { name: Home, c

  • Vue项目全局配置页面缓存之按需读取缓存的实现详解

    写在前面 一个web app的实际使用场景中,有一些情景的交互要求,是记录用户的浏览状态的.最常见的就是在列表页进入详情页之后,再返回到列表页,用户希望返回到进入详情页之前的状态继续操作.但是有些使用场景,用户又是希望能够获取最新的数据,例如同级列表页之间切换的时候. 如此,针对上述两种使用场景,需要实现按需读取页面缓存.由于SPA应用的路由逻辑也是在前端实现的,因此可以在前端对路由的逻辑进行设置以实现所需效果. 使用技术 Vue.js作为主要框架 Vue-router作为前端路由管理器 Vue

  • Vue2.0 实现页面缓存和不缓存的方式

    1.在app中设置需要缓存的div <keep-alive>//缓存的页面 <router-view v-if="$route.meta.keepAlive"></router-view> </keep-alive> <router-view v-if="!$route.meta.keepAlive"></router-view>//不缓存的页面 2.在路由router.js中设置.vue页面是

随机推荐