前言:
我们通常会在用到 PageView +
BottomNavigationBar
或者 TabBarView + TabBar 的情况. 但是大家发现当我们切换到另一页面的时候, 前一个页面就会被销毁, 当再返回前一页时, 页面会被重建. 随之数据要重新加载, 控件要重新渲染 带来了极不好的用户体验.
下面是一些解决方案:
解决方案一:
使用
- AutomaticKeepAliveClientMixin
- (官方推荐做法)
由于 TabBarView 内部也是用的是 PageView, 因此两者的解决方式相同, 下面以 PageView 为例
但这种方式在老版本并不好用, 需要更新到比较新的版本.
Flutter 0.5.8-pre.277 channel master https://github.com/flutter/flutter.git 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
例如下面的 Example:
- 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
要实现
AutomaticKeepAliveClientMixin
不是 PageView 所在的 Widget, 而是 PageView 的 children 所在的 Widget
如果上面这个方法对你不起作用, 或者你暂时不打算升级 Flutter 版本, 可以使用下面的这个方法.
解决方案二:
将 PageView 的代码拷贝出来, 然后把其中 Viewport 的属性 cacheExtent 设置成一个比较大的数
如果是 TabBarView 也需要进行此步操作, 后面会讲解
- ...
- child: new Scrollable(
- axisDirection: axisDirection,
- controller: widget.controller,
- physics: physics,
- viewportBuilder: (BuildContext context, ViewportOffset position) {
- return new Viewport(
- cacheExtent: 250.0,
- axisDirection: axisDirection,
- offset: position,
- slivers: <Widget>[
- new SliverFillViewport(
- viewportFraction: widget.controller.viewportFraction,
- delegate: widget.childrenDelegate
- ),
- ],
- );
- },
- ),
- ...
复制代码
如果不对 cacheExtent 赋值, 那么最终它的默认值是
250.0
, 但在 PageView 源码中官方写死了 0.0
具体实现:
在自己的项目里新建一个 dart 文件, 例如: my_page_view.dart
拷贝 PageView 的源码到我们自己的这个文件中, 注意: 只需要拷贝 PageView 和_PageViewState 的代码就行了, 不需要把整个文件的内容都拷贝出去
遇到报错是导包的问题, 根据提示进行导包即可
- import 'package:flutter/material.dart';
- import 'package:flutter/rendering.dart';
复制代码
修改 cacheExtent 的值
在我们自己的这个 PageView 的时候, 可能会出现导包冲突, 可用 hinde 关键字将系统的隐藏掉, 或者把 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, 那么滑动到第二页时不会被销毁, 直到完全滑动到第三页时才会被销毁.
综上所述, 如果你想无脑缓存所有页面, 那么给一个 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 NotificationListener<ScrollNotification>(
- onNotification: (ScrollNotification notification) {
- if (notification.depth == 0 &&
- widget.onPageChanged != null &&
- notification is ScrollUpdateNotification) {
- final PageMetrics metrics = notification.metrics;
- final int currentPage = metrics.page.round();
- if (currentPage != _lastReportedPage) {
- _lastReportedPage = currentPage;
- widget.onPageChanged(currentPage);
- }
- }
- return false;
- },
- child: new Scrollable(
- axisDirection: axisDirection,
- controller: widget.controller,
- physics: physics,
- viewportBuilder: (BuildContext context, ViewportOffset position) {
- 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
如果是 TabBarView
由于 TabBarView 内部就是封装了一个 PageView 因此, 我们先要像上面所述那模样修改 PageView, 然后再将 TabBarView 内的 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;
复制代码
从 0 到 1: 我的 Flutter 技术实践 | 掘金技术征文, 征文活动正在进行中
来源: https://juejin.im/post/5b73c3b3f265da27d701473a