Flutter: PageView/TabBarView等控件保存状态的问题解决方案

18,691 阅读5分钟

转载请标明出处: juejin.cn/post/684490…
本文出自:Wos的主页

最后更新: 2019-06-11


前言:

通常在用到 PageView + BottomNavigationBar 或者 TabBarView + TabBar 的时候大家会发现当切换到另一页面的时候, 前一个页面就会被销毁, 再返回前一页时, 页面会被重建, 随之数据会重新加载, 控件会重新渲染 带来了极不好的用户体验.

下面是一些解决方案:

解决方案一:

使用 AutomaticKeepAliveClientMixin (官方推荐做法)

由于TabBarView内部也是用的是PageView, 因此两者的解决方式相同. 下面以PageView为例

这种方式在老版本并不好用, 需要更新到比较新的版本.

Flutter 0.5.8-pre.277 • channel master • github.com/flutter/flu… Framework • revision e5432a2843 (6 days ago) • 2018-08-08 16:45:08 -0700 Engine • revision 3777931801 Tools • Dart 2.0.0-dev.69.5.flutter-eab492385c

以上我在写这篇文章的时候的版本, 但具体以哪个版本为分界线我不清楚.

通过以下命令可以查看Flutter的版本

flutter --version

通过以下命令可以切换Flutter Channel(对应于它的git的branch)

flutter channel master

master 是 channel 的名字, 目前有: beta devmaster. 从代码更新频率上讲 master > dev > beta

具体做法:

PageView(或TabBarView) 的 children 的State 继承 AutomaticKeepAliveClientMixin

示例如下:

import 'package:flutter/material.dart';

main() {
  runApp(MaterialApp(
    home: Test6(),
  ));
}

class Test6 extends StatefulWidget {
  @override
  Test6State createState() {
    return new Test6State();
  }
}

class Test6State extends State<Test6> {
  PageController _pageController;

  @override
  void initState() {
    super.initState();
    _pageController = PageController();
  }

  @override
  Widget build(BuildContext context) {
    List<int> pages = [1, 2, 3, 4];
    List<int> data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
    return Scaffold(
      appBar: AppBar(),
      body: PageView(
        children: pages.map((i) {
          return Container(
            height: double.infinity,
            color: Colors.red,
            child: Test6Page(i, data),
          );
        }).toList(),
        controller: _pageController,
      ),
    );
  }
}

class Test6Page extends StatefulWidget {
  final int pageIndex;
  final List<int> data;

  Test6Page(this.pageIndex, this.data);

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

class _Test6PageState extends State<Test6Page> with AutomaticKeepAliveClientMixin {
  @override
  void initState() {
    super.initState();
    print('initState');
  }

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

  @override
  Widget build(BuildContext context) {
    return ListView(
      children: widget.data.map((n) {
        return ListTile(
          title: Text("第${widget.pageIndex}页的第$n个条目"),
        );
      }).toList(),
    );
  }

  @override
  bool get wantKeepAlive => true;
}

总结:

  1. PageView 的children需要继承自 StatefulWidget
  2. PageView 的children对应的 State 需要继承自 AutomaticKeepAliveClientMixin

注意: 上述方法有一些朋友反馈说在一些情况下, 例如跳转进入子页面在返回时, 页面还是会刷新.

这时你需要检查你混入了 AutomaticKeepAliveClientMixin 的 State

AutomaticKeepAliveClientMixin 源码中, 若干方法都调用了 _ensureKeepAlive(), 这是之所以能够保持状态的关键.

这其中包括了 build(BuildContext context). 而在此之前你很可能就已经实现了 State 的 build 方法, 因此没有注意到 AutomaticKeepAliveClientMixin 的 build 方法有一个 @mustCallSuper 的注解.

那么解决方法也就是在你实现的 build 方法顶部加上 super.build(context);

另外也需要注意其它涉及到的方法是否都按照要求实现, 善用 Lint 工具.

特别感谢 静坐常思过君 同学提供上述问题的解决方案.


如果第一个方法对你不起作用, 或者你暂时不打算升级Flutter版本, 可以尝试下面的这个方法.

解决方案二:

PageView 的代码拷贝出来, 然后把其中Viewport的属性 cacheExtent 设置成一个比较大的数

PageView 源码中官方写死了cacheExtent: 0.0. 如果将这个赋值删掉, 那么最终会使用默认值 250.0 可以缓存一个Widget

如果是TabBarView也需要进行此步操作, 后面会讲解

具体实现:

  1. 在自己的项目里新建一个dart文件, 例如: my_page_view.dart
  2. 拷贝PageView的源码到这个文件中, 注意: 只需要拷贝PageView_PageViewState的代码就行了, 不需要把整个文件的内容都拷贝出去
  3. 如遇报错, 应该是导包的问题, 根据提示进行导包即可
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
  1. 修改cacheExtent的值
  2. 在使用 my_page_view.dart 时, 可能会出现导包冲突, 可用hide关键字将系统的隐藏掉, 或者把 PageView 重命名一下
import 'package:flutter/material.dart' hide PageView;

经测试发现, cacheExtent 的作用是: 当偏移 Pw + cacheExtent 时销毁P (P表示当前页面,Pw是当前页面的宽度)

举个例子: 如果PageView有三个页面, 默认打开时在第一页, cacheExtent: 0.0 则当向右滑动到达第一个页面的宽度时, 第一个页面被销毁. 这就是为什么PageView不能保留页面状态

同理, 如果 cacheExtent: 1.0 那么当滑到第二页时, 第一页还没销毁, 但只需要再向右滑动1(理论像素)的距离, 第一个页面就会被销毁.

再比如, 如果 cacheExtent页面宽度 - 1, 那么直到完全滑动到第三页时第一页才会被销毁.

综上所述, 如想缓存所有页面, 那么使用 cacheExtent: double.infinity 即可

但如果想更灵活一些, 可以按照以下方法"稍作加工"

class PageView extends StatefulWidget {

  final int cacheCount;
  
  ...
}

class _PageViewState extends State<PageView> {
  ...

  @override
  Widget build(BuildContext context) {
    ...
    
    return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
      ...
            return new Viewport(
              cacheExtent: widget.cacheCount * constraints.maxWidth - 1,
              axisDirection: axisDirection,
              offset: position,
              slivers: <Widget>[
                new SliverFillViewport(
                    viewportFraction: widget.controller.viewportFraction,
                    delegate: widget.childrenDelegate),
              ],
            );
      ...
  }
}
  1. PageView加上一个cacheCount的属性, 表示缓存的页面的数量. 记得给所有构造都加上这个属性
  2. _PageViewStatebuild方法返回的Widget外面套了一个 LayoutBuilder 用来获取控件的宽高, 然后修改 cacheExtentwidget.cacheCount * constraints.maxWidth - 1

然后在使用时, 为 cacheCount 赋值即可

如果是TabBarView

由于 TabBarView 内部封装了一个 PageView 因此先要像上面所述那样修改 PageView, 然后再将 TabBarView 内的 PageView 替换成修改后的 PageView

  1. PageView, 将 TabBarView_TabBarViewState 以及这两个类用到的私有常量(_kTabBarViewPhysics)拷贝出来.
  2. 导包解决错误
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_meizi/component/my_page_view.dart';
  1. 在导包上用关键字hide隐藏系统自带PageView控件
import 'package:flutter/material.dart' hide PageView;