1. 程式人生 > >Flutter: PageView/TabBarView 等控制元件儲存狀態的問題解決方案

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;

複製程式碼