Flutter: PageView/TabBarView等控制元件儲存狀態的問題解決方案 | 掘金技術徵文

Wos發表於2018-08-17

轉載請標明出處: juejin.im/post/5b73c3…
本文出自:Wos的主頁

前言:

通常在用到 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

如果第一個方法對你不起作用, 或者你暫時不打算升級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;
複製程式碼

從 0 到 1:我的 Flutter 技術實踐 | 掘金技術徵文,徵文活動正在進行中

相關文章