转载请标明出处: 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
dev
和 master
. 从代码更新频率上讲 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;
}
总结:
PageView
的children需要继承自StatefulWidget
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也需要进行此步操作, 后面会讲解
具体实现:
- 在自己的项目里新建一个dart文件, 例如:
my_page_view.dart
- 拷贝PageView的源码到这个文件中, 注意: 只需要拷贝
PageView
和_PageViewState
的代码就行了, 不需要把整个文件的内容都拷贝出去 - 如遇报错, 应该是导包的问题, 根据提示进行导包即可
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
- 修改
cacheExtent
的值 - 在使用
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),
],
);
...
}
}
- 给
PageView
加上一个cacheCount
的属性, 表示缓存的页面的数量. 记得给所有构造都加上这个属性 _PageViewState
的build
方法返回的Widget外面套了一个LayoutBuilder
用来获取控件的宽高, 然后修改cacheExtent
为widget.cacheCount * constraints.maxWidth - 1
然后在使用时, 为 cacheCount
赋值即可
如果是TabBarView
由于 TabBarView
内部封装了一个 PageView
因此先要像上面所述那样修改 PageView
, 然后再将 TabBarView
内的 PageView
替换成修改后的 PageView
- 同
PageView
, 将TabBarView
和_TabBarViewState
以及这两个类用到的私有常量(_kTabBarViewPhysics
)拷贝出来. - 导包解决错误
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_meizi/component/my_page_view.dart';
- 在导包上用关键字
hide
隐藏系统自带PageView
控件
import 'package:flutter/material.dart' hide PageView;