轉載請標明出處: 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
dev
和 master
. 從程式碼更新頻率上講 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;
}
複製程式碼
總結:
PageView
的children需要繼承自StatefulWidget
PageView
的children對應的State
需要繼承自AutomaticKeepAliveClientMixin
如果第一個方法對你不起作用, 或者你暫時不打算升級Flutter版本, 可以嘗試下面的這個方法.
解決方案二:
將 PageView
的程式碼拷貝出來, 然後把其中Viewport
的屬性 cacheExtent
設定成一個比較大的數
PageView
原始碼中官方寫死了cacheExtent: 0.0
. 如果將這個賦值刪掉, 那麼最終會使用預設值 250.0
可以快取一個Widget
如果是TabBarView也需要進行此步操作, 後面會講解
具體實現:
- 在自己的專案裡新建一個dart檔案, 例如:
my_page_view.dart
- 拷貝PageView的原始碼到這個檔案中, 注意: 只需要拷貝
PageView
和_PageViewState
的程式碼就行了, 不需要把整個檔案的內容都拷貝出去 - 如遇報錯, 應該是導包的問題, 根據提示進行導包即可
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
複製程式碼
- 修改
cacheExtent
的值 - 在使用
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),
],
);
...
}
}
複製程式碼
- 給
PageView
加上一個cacheCount
的屬性, 表示快取的頁面的數量. 記得給所有構造都加上這個屬性 _PageViewState
的build
方法返回的Widget外面套了一個LayoutBuilder
用來獲取控制元件的寬高, 然後修改cacheExtent
為widget.cacheCount * constraints.maxWidth - 1
然後在使用時, 為 cacheCount
賦值即可
如果是TabBarView
由於 TabBarView
內部封裝了一個 PageView
因此先要像上面所述那樣修改 PageView
, 然後再將 TabBarView
內的 PageView
替換成修改後的 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;
複製程式碼