Flutter之PageView頁面緩存與KeepAlive
正文
如果要實現(xiàn)頁面切換和 Tab 布局,我們可以使用 PageView 組件。需要注意,PageView 是一個非常重要的組件,因為在移動端開發(fā)中很常用,比如大多數(shù) App 都包含 Tab 換頁效果、圖片輪動以及抖音上下滑頁切換視頻功能等等,這些都可以通過 PageView 輕松實現(xiàn)。
構(gòu)造函數(shù)
PageView({ Key? key, this.scrollDirection = Axis.horizontal, // 滑動方向 this.reverse = false, PageController? controller, this.physics, List<Widget> children = const <Widget>[], this.onPageChanged, //每次滑動是否強(qiáng)制切換整個頁面,如果為false,則會根據(jù)實際的滑動距離顯示頁面 this.pageSnapping = true, //主要是配合輔助功能用的,后面解釋 this.allowImplicitScrolling = false, //后面解釋 this.padEnds = true, })
我們看一個 Tab 切換的實例,為了突出重點,我們讓每個 Tab 頁都只顯示一個數(shù)字。
// Tab 頁面 class Page extends StatefulWidget { const Page({ Key? key, required this.text }) : super(key: key); final String text; @override _PageState createState() => _PageState(); } class _PageState extends State<Page> { @override Widget build(BuildContext context) { print("build ${widget.text}"); return Center(child: Text("${widget.text}", textScaleFactor: 5)); } }
@override Widget build(BuildContext context) { var children = <Widget>[]; // 生成 10 個 Tab 頁 for (int i = 0; i < 10; ++i) { children.add( Page( text: '$i')); } return PageView( // scrollDirection: Axis.vertical, // 滑動方向為垂直方向 children: children, ); }
如果將 PageView 的滑動方向指定為垂直方向(上面代碼中注釋部分),則會變?yōu)樯舷禄瑒忧袚Q頁面。
頁面緩存
我們在運(yùn)行上面示例時,可能已經(jīng)發(fā)現(xiàn):每當(dāng)頁面切換時都會觸發(fā)新 Page 頁的 build,比如我們從第一頁滑到第二頁,然后再滑回第一頁時,控制臺打印如下:
flutter: build 0
flutter: build 1
flutter: build 0
可見 PageView 默認(rèn)并沒有緩存功能,一旦頁面滑出屏幕它就會被銷毀, 和ListView/GridView 不一樣,在創(chuàng)建 ListView/GridView 時我們可以手動指定 ViewPort 之外多大范圍內(nèi)的組件需要預(yù)渲染和緩存(通過 cacheExtent 指定),只有當(dāng)組件滑出屏幕后又滑出預(yù)渲染區(qū)域,組件才會被銷毀,但是不幸的是 PageView 并沒有 cacheExtent 參數(shù)!但是在真實的業(yè)務(wù)場景中,對頁面進(jìn)行緩存是很常見的一個需求,比如一個新聞 App,下面有很多頻道頁,如果不支持頁面緩存,則一旦滑到新的頻道舊的頻道頁就會銷毀,滑回去時又得重新請求數(shù)據(jù)和構(gòu)建頁面,這樣極度消耗性能。
按道理 cacheExtent 是 Viewport 的一個配置屬性,且 PageView 也是要構(gòu)建 Viewport 的,那么為什么就不能透傳一下這個參數(shù)呢?于是筆者帶著這個疑問看了一下 PageView 的源碼,發(fā)現(xiàn)在 PageView 創(chuàng)建Viewport 的代碼中是這樣的:
child: Scrollable( ... viewportBuilder: (BuildContext context, ViewportOffset position) { return Viewport( // TODO(dnfield): we should provide a way to set cacheExtent // independent of implicit scrolling: // https://github.com/flutter/flutter/issues/45632 cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0, cacheExtentStyle: CacheExtentStyle.viewport, ... ); }, )
我們發(fā)現(xiàn) 雖然 PageView 沒有透傳 cacheExtent,但是卻在allowImplicitScrolling 為 true 時設(shè)置了預(yù)渲染區(qū)域,注意,此時的緩存類型為 CacheExtentStyle.viewport,則 cacheExtent 則表示緩存的長度是幾個 Viewport 的寬度,cacheExtent 為 1.0,則代表前后各緩存一個頁面寬度,即前后各一頁。既然如此,那我們將 PageView 的 allowImplicitScrolling 置為 true 則不就可以緩存前后兩頁了?我們修改代碼,然后運(yùn)行示例,發(fā)現(xiàn)在第一頁時,控制臺打印信息如下:
flutter: build 0
flutter: build 1 // 預(yù)渲染第二頁
當(dāng)再滑回第一頁時,控制臺信息不變,這也就意味著第一頁緩存成功,它沒有被重新構(gòu)建。但是如果我們從第二頁滑到第三頁,然后再滑回第一頁時,控制臺又會輸出 ”build 0“,這也符合預(yù)期,因為我們之前分析的就是設(shè)置 allowImplicitScrolling 置為 true 時就只會緩存前后各一頁,所以滑到第三頁時,第一頁就會銷毀。
能緩存前后各一頁也貌似比不能緩存好一點,但還是不能徹底解決不了我們的問題。為什么明明就是順手的事, flutter 就不讓開發(fā)者指定緩存策略呢?然后我們翻譯一下源碼中的注釋:
Todo:我們應(yīng)該提供一種獨(dú)立于隱式滾動(implicit scrolling)的設(shè)置 cacheExtent 的機(jī)制。
放開 cacheExtent 透傳不就是順手的事么,為什么還要以后再做,是有什么難題么?這就要看看 allowImplicitScrolling 到底是什么了,根據(jù)文檔以及注釋中 issue 的鏈接,發(fā)現(xiàn)PageView 中設(shè)置 cacheExtent 會和 iOS 中 輔助功能有沖突(讀者可以先不用關(guān)注),所以暫時還沒有什么好的辦法??吹竭@可能國內(nèi)的很多開發(fā)者要說我們的 App 不用考慮輔助功能,既然如此,那問題很好解決,將 PageView 的源碼拷貝一份,然后透傳 cacheExtent 即可。 考源碼的方式雖然很簡單,但畢竟不是正統(tǒng)做法,那有沒有更通用的方法嗎?有!可滾動組件提供了一種通用的緩存子項的解決方案,答案是有的。
KeepAlive
AumaticKeepAlive
的組件的主要作用是將列表項的根 RenderObject 的 keepAlive 按需自動標(biāo)記 為 true 或 false。為了方便敘述,我們可以認(rèn)為根 RenderObject 對應(yīng)的組件就是列表項的根 Widget,代表整個列表項組件,同時我們將列表組件的 Viewport區(qū)域 + cacheExtent(預(yù)渲染區(qū)域)稱為加載區(qū)域 :
- 當(dāng) keepAlive 標(biāo)記為 false 時,如果列表項滑出加載區(qū)域時,列表組件將會被銷毀。
- 當(dāng) keepAlive 標(biāo)記為 true 時,當(dāng)列表項滑出加載區(qū)域后,Viewport 會將列表組件緩存起來;當(dāng)列表項進(jìn)入加載區(qū)域時,Viewport 從先從緩存中查找是否已經(jīng)緩存,如果有則直接復(fù)用,如果沒有則重新創(chuàng)建列表項。
那么 AutomaticKeepAlive 什么時候會將列表項的 keepAlive 標(biāo)記為 true 或 false 呢?答案是開發(fā)者說了算!Flutter 中實現(xiàn)了一套類似 C/S 的機(jī)制,AutomaticKeepAlive 就類似一個 Server,它的子組件可以是 Client,這樣子組件想改變是否需要緩存的狀態(tài)時就向 AutomaticKeepAlive 發(fā)一個通知消息(KeepAliveNotification),AutomaticKeepAlive 收到消息后會去更改 keepAlive 的狀態(tài),如果有必要同時做一些資源清理的工作(比如 keepAlive 從 true 變?yōu)?false 時,要釋放緩存)。
我們基于上面 PageView 示例,實現(xiàn)頁面緩存,根據(jù)上面的描述實現(xiàn)思路就很簡單了:讓Page 頁變成一個 AutomaticKeepAlive Client 即可。為了便于開發(fā)者實現(xiàn),F(xiàn)lutter 提供了一個 AutomaticKeepAliveClientMixin ,我們只需要讓 PageState 混入這個 mixin,且同時添加一些必要操作即可:
class _PageState extends State<Page> with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); // 必須調(diào)用 return Center(child: Text("${widget.text}", textScaleFactor: 5)); } @override bool get wantKeepAlive => true; // 是否需要緩存 }
代碼很簡單,我們只需要提供一個 wantKeepAlive,它會表示 AutomaticKeepAlive 是否需要緩存當(dāng)前列表項;另外我們必須在 build 方法中調(diào)用一下 super.build(context),該方法實現(xiàn)在 AutomaticKeepAliveClientMixin 中,功能就是根據(jù)當(dāng)前 wantKeepAlive 的值給 AutomaticKeepAlive 發(fā)送消息,AutomaticKeepAlive 收到消息后就會開始工作。
現(xiàn)在我們重新運(yùn)行一下示例,發(fā)現(xiàn)每個 Page 頁只會 build 一次,緩存成功了。需要注意,如果我們采用 PageView.custom 構(gòu)建頁面時沒有給列表項包裝 AutomaticKeepAlive 父組件,則上述方案不能正常工作,因為此時Client 發(fā)出消息后,找不到 Server,404 了.
KeepAliveWrapper
雖然我們可以通過 AutomaticKeepAliveClientMixin 快速的實現(xiàn)頁面緩存功能,但是通過混入的方式實現(xiàn)不是很優(yōu)雅,因為必須更改 Page 的代碼,有侵入性,這就導(dǎo)致不是很靈活,比如一個組件能同時在列表中和列表外使用,為了在列表中緩存它,則我們必須實現(xiàn)兩份。為了解決這個問題,筆者封裝了一個 KeepAliveWrapper 組件,如果哪個列表項需要緩存,只需要使用 KeepAliveWrapper 包裹一下它即可。
@override Widget build(BuildContext context) { var children = <Widget>[]; for (int i = 0; i < 10++i) { //只需要用 KeepAliveWrapper 包裝一下即可 children.add(KeepAliveWrapper(child:Page( text: '$i')); } return PageView(children: children); }
下面是 KeepAliveWrapper 的實現(xiàn)源碼:
class KeepAliveWrapper extends StatefulWidget { const KeepAliveWrapper({ Key? key, this.keepAlive = true, required this.child, }) : super(key: key); final bool keepAlive; final Widget child; @override _KeepAliveWrapperState createState() => _KeepAliveWrapperState(); } class _KeepAliveWrapperState extends State<KeepAliveWrapper> with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); return widget.child; } @override void didUpdateWidget(covariant KeepAliveWrapper oldWidget) { if(oldWidget.keepAlive != widget.keepAlive) { // keepAlive 狀態(tài)需要更新,實現(xiàn)在 AutomaticKeepAliveClientMixin 中 updateKeepAlive(); } super.didUpdateWidget(oldWidget); } @override bool get wantKeepAlive => widget.keepAlive; }
可以看出也是基于AutomaticKeepAliveClientMixin
實現(xiàn)了 bool get wantKeepAlive => widget.keepAlive;
并且包裹了子組件。
總結(jié)
本章主要介紹了Pageview頁面緩存的兩種方式,AutomaticKeepAlive和KeepAliveWrapper包裹。另外還需要關(guān)注Viewport區(qū)域 + cacheExtent的緩存策略和場景。更多關(guān)于Flutter PageView頁面緩存的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
IOS UI學(xué)習(xí)教程之設(shè)置UITextField各種屬性
這篇文章主要為大家詳細(xì)介紹了IOS UI學(xué)習(xí)教程之設(shè)置UITextField各種屬性,感興趣的小伙伴們可以參考一下2016-03-03舉例講解iOS中延遲加載和上拉刷新/下拉加載的實現(xiàn)
這篇文章主要介紹了舉例講解iOS中延遲加載和上拉刷新/下拉加載的實現(xiàn),語言依然為傳統(tǒng)的Objective-C,需要的朋友可以參考下2015-09-09iOS block循環(huán)引用詳解及常見誤區(qū)
這篇文章主要介紹了iOS block循環(huán)引用詳解和應(yīng)用,常見誤區(qū)詳解,本文通過實例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2021-08-08iOS使用音頻處理框架The Amazing Audio Engine實現(xiàn)音頻錄制播放
這篇文章主要為大家詳細(xì)介紹了iOS使用音頻處理框架The Amazing Audio Engine實現(xiàn)音頻錄制播放,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-04-04iOS開發(fā)教程之UIView和UIViewController的生命周期詳解
UIViewController是IOS程序中的一個重要組成部分,下面這篇文章主要給大家介紹了關(guān)于iOS開發(fā)教程之UIView和UIViewController的生命周期的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下2018-04-04