Flutter: PageView/TabBarView 等控制元件儲存狀態的問題解決方案
前言:
我們通常會在用到 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;
複製程式碼