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

前言:

我们通常会在用到 PageView +

BottomNavigationBar

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

下面是一些解决方案:

解决方案一:

使用

 
  1. AutomaticKeepAliveClientMixin
  2. (官方推荐做法)

由于 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:

 
  1. import 'package:flutter/material.dart';
  2. main() {
  3. runApp(MaterialApp(
  4. home: Test6(),
  5. ));
  6. }
  7. class Test6 extends StatefulWidget {
  8. @override
  9. Test6State createState() {
  10. return new Test6State();
  11. }
  12. }
  13. class Test6State extends State<Test6> {
  14. PageController _pageController;
  15. @override
  16. void initState() {
  17. super.initState();
  18. _pageController = PageController();
  19. }
  20. @override
  21. Widget build(BuildContext context) {
  22. List<int> pages = [1, 2, 3, 4];
  23. List<int> data = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
  24. return Scaffold(
  25. appBar: AppBar(),
  26. body: PageView(
  27. children: pages.map((i) {
  28. return Container(
  29. height: double.infinity,
  30. color: Colors.red,
  31. child: Test6Page(i, data),
  32. );
  33. }).toList(),
  34. controller: _pageController,
  35. ),
  36. );
  37. }
  38. }
  39. class Test6Page extends StatefulWidget {
  40. final int pageIndex;
  41. final List<int> data;
  42. Test6Page(this.pageIndex, this.data);
  43. @override
  44. _Test6PageState createState() => _Test6PageState();
  45. }
  46. class _Test6PageState extends State<Test6Page> with AutomaticKeepAliveClientMixin {
  47. @override
  48. void initState() {
  49. super.initState();
  50. print('initState');
  51. }
  52. @override
  53. void dispose() {
  54. print('dispose');
  55. super.dispose();
  56. }
  57. @override
  58. Widget build(BuildContext context) {
  59. return ListView(
  60. children: widget.data.map((n) {
  61. return ListTile(
  62. title: Text("第 ${widget.pageIndex} 页的第 $n 个条目"),
  63. );
  64. }).toList(),
  65. );
  66. }
  67. @override
  68. bool get wantKeepAlive => true;
  69. }

复制代码

总结:

PageView 的 children 需要是一个 StatefulWidget

要实现

AutomaticKeepAliveClientMixin

不是 PageView 所在的 Widget, 而是 PageView 的 children 所在的 Widget

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

解决方案二:

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

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

 
  1. ...
  2. child: new Scrollable(
  3. axisDirection: axisDirection,
  4. controller: widget.controller,
  5. physics: physics,
  6. viewportBuilder: (BuildContext context, ViewportOffset position) {
  7. return new Viewport(
  8. cacheExtent: 250.0,
  9. axisDirection: axisDirection,
  10. offset: position,
  11. slivers: <Widget>[
  12. new SliverFillViewport(
  13. viewportFraction: widget.controller.viewportFraction,
  14. delegate: widget.childrenDelegate
  15. ),
  16. ],
  17. );
  18. },
  19. ),
  20. ...

复制代码

如果不对 cacheExtent 赋值, 那么最终它的默认值是

250.0

, 但在 PageView 源码中官方写死了 0.0

具体实现:

在自己的项目里新建一个 dart 文件, 例如: my_page_view.dart

拷贝 PageView 的源码到我们自己的这个文件中, 注意: 只需要拷贝 PageView 和_PageViewState 的代码就行了, 不需要把整个文件的内容都拷贝出去

遇到报错是导包的问题, 根据提示进行导包即可

 
  1. import 'package:flutter/material.dart';
  2. 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 就好了

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

 
  1. class PageView extends StatefulWidget {
  2. // 记得给所有的构造都加上这个属性
  3. final int cacheCount;
  4. ...
  5. }
  6. class _PageViewState extends State<PageView> {
  7. ...
  8. @override
  9. Widget build(BuildContext context) {
  10. ...
  11. return LayoutBuilder(builder: (BuildContext context, BoxConstraints constraints) {
  12. return new NotificationListener<ScrollNotification>(
  13. onNotification: (ScrollNotification notification) {
  14. if (notification.depth == 0 &&
  15. widget.onPageChanged != null &&
  16. notification is ScrollUpdateNotification) {
  17. final PageMetrics metrics = notification.metrics;
  18. final int currentPage = metrics.page.round();
  19. if (currentPage != _lastReportedPage) {
  20. _lastReportedPage = currentPage;
  21. widget.onPageChanged(currentPage);
  22. }
  23. }
  24. return false;
  25. },
  26. child: new Scrollable(
  27. axisDirection: axisDirection,
  28. controller: widget.controller,
  29. physics: physics,
  30. viewportBuilder: (BuildContext context, ViewportOffset position) {
  31. return new Viewport(
  32. cacheExtent: widget.cacheCount * constraints.maxWidth - 1,
  33. axisDirection: axisDirection,
  34. offset: position,
  35. slivers: <Widget>[
  36. new SliverFillViewport(
  37. viewportFraction: widget.controller.viewportFraction,
  38. delegate: widget.childrenDelegate),
  39. ],
  40. );
  41. },
  42. ),
  43. );
  44. });
  45. }
  46. }

复制代码

给 PageView 加上一个 cacheCount 的属性, 表示缓存的页面的数量. 记得给所有构造都加上

在_PageViewState 的 build 方法返回的 Widget 外面套了一个 LayoutBuilder 用来获取控件的宽高, 然后修改 cacheExtent 为

widget.cacheCount * constraints.maxWidth - 1

如果是 TabBarView

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

同 PageView 那样, 我们将 TabBarView 和 _TabBarViewState 已经这两个类用到的私有常量 (

_kTabBarViewPhysics

) 拷贝出来.

导包解决错误

 
  1. import 'dart:async';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter_meizi/component/my_page_view.dart';

复制代码

在导包上用关键字 hide 隐藏系统自带 PageView 控件

import 'package:flutter/material.dart' hide PageView;

复制代码

猜你喜欢

转载自blog.csdn.net/nimeghbia/article/details/82881927
今日推荐