持续创造,加快成长!这是我参与「日新计划 10 月更文挑战」的第29天,点击检查活动详情

如果要完成页面切换和 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,
  );
}

29、Flutter之PageView与页面缓存与KeepAlive

如果将 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 收到音讯后就会开始作业。

29、Flutter之PageView与页面缓存与KeepAlive

现在咱们从头运转一下示例,发现每个 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的缓存策略和场景。