Flutter上的一個日曆控件,能夠定製成本身想要的樣子。git
實現日曆並不難,主要是以前實現的性能有點差,進行了下優化。這篇文章主要記錄一下本身對這個日曆項目的優化過程。github
項目地址web
日曆支持web預覽:點擊此處進入預覽編程
下圖就是項目的總體結構,沒啥特殊的,就是寫一個日曆須要的一些數據。瀏覽器
優化前,真tm是一片紅。性能優化
進行各類優化後,最終的性能以下面幾個圖所示。總體性能還好,頁面間切換也沒那麼卡來。每幀的耗時大部分小於16ms,達到指望,還能夠繼續優化下。bash
以前日曆的配置信息全都是放在controller裏面的,並且日曆可配置的信息會有不少,感受有點亂。此次將配置的信息都抽放到一個CalendarConfiguration對象中。而且這個CalendarConfiguration對象存放在頂層的provider狀態類中,可讓子Widget直接獲取到配置信息。網絡
引進provider狀態管理框架,一方面是能夠避免數據各類嵌套傳遞,一方面是實現局部刷新提升性能。app
關於provider的使用:Flutter | 狀態管理指南篇——Provider框架
各類數據和狀態都須要一層一層往下傳遞給子Widget,有點噁心。
建立狀態類CalendarProvider,用於共享日曆的數據和狀態,子widget能夠直接獲取到CalendarProvider中的數據。代碼比以前好看一點,不事後面仍是得繼續優化。
沒有了代碼嵌套,構造方法簡單了不少。在子組件的build方法裏,也能夠獲取到各類狀態數據。相關文章:Flutter 異步編程:Future、Isolate 和事件循環
Dart是一種單線程語言,Isolate就是Dart中的線程。
默認的Flutter代碼是運行在同一個isolate裏面的。每一個Isolate都有着本身的事件循環EventLoop和兩個隊列(MicroTask和Event)。以下圖所示,MicroTask隊列的優先級優先於Event隊列,當沒有MicroTask事件的時候,纔會去執行Event隊列中的第一項。
注意:Future操做也是經過Event隊列處理。Future和async並不是並行執行,而是遵循事件循環處理事件的順序規則執行。 若是繁重的處理可能須要一些時間才能完成,而且可能影響應用的性能,考慮使用 Isolate。 因此,能夠利用多個Isolate來實現真正的並行處理。
Flutter提供了一個compute的方法,讓咱們可用來直接建立一個isolate。Compute函數對isolate的建立和底層的消息傳遞進行了封裝,使得咱們沒必要關係底層的實現,只須要關注功能實現。 使用Compute寫isolates
Isolate的使用場景
因此一些比較耗時的操做,咱們能夠放在另外一個isolate中進行並行執行。
在日曆項目中的應用
好比我準備顯示月視圖,須要進行數據的加載拿到42個item。就須要去計算這個月對應的是42個item,以及去計算出每一個item所對應的DateModel,以及各類所須要的信息。這個過程是比較耗時的,因此我就使用compute將這個操做放在另外一個isolate裏面進行操做。
Dart中的某個變量,默認都有setter和getter方法,getter方法就是用來獲取變量的值。
若是這個變量的值是須要一系列計算(可能比較耗時)後才能獲得結果。 若是在建立這個對象的時候就去計算結果並賦值給這個變量,就會花費額外的一些時間。也有可能計算了,可是這個對象後面也不會用到,那就很不必了。
因此可使用相似下面的寫法,實現相似懶加載的功能。調用getter方法的時候,纔去判斷值是否爲空,爲空的話纔開始進行數據計算。
每一個DateModel都包含了一大堆屬性,須要我去計算,好比農曆、傳統節日、24節氣。這些的計算是比較複雜和耗時的,因此我就將部分屬性的計算操做放到對應getter方法中,這樣的話,就不用在一開始加載數據的時候,將全部的屬性都進行計算。
呵呵,以前點擊item後,而後無論三七二之一,調用setState方法去刷新整個日曆的狀態,搞定。可想而知,性能確定會差點。
提升Build效率的一種方法就是下降遍歷的出發點。直接在日曆Widget內調用它的setState的話,那rebuild的時候,就須要將整個日曆Widget樹進行遍歷刷新。
因此這裏的作法是將日曆的item抽成一個StatefulWidget,這樣的話,若是調用日曆item的setState方法的話,就只會刷新這個item,實現將刷新範圍縮小到item級別。
注意:這裏要判斷mounted後纔去調用setState方法。由於有可能這個節點已經從element樹移除了,這個時候若是調用setState的話,就會報錯。在日常的開發中,也要注意這種問題。好比在獲取網絡數據的時候,若是當前頁面被dispose了,等接口的數據返回後,直接調用setState的話就會報錯。
Exception caught by gesture
The following assertion was thrown while handling a gesture:
setState() called after dispose()
複製代碼
多選模式就很簡單,每一個item,都利用GestureDetector監聽日曆item的點擊事件,而後調用setState方法刷新自身就行。
單選模式,好比咱們選中某一個item,須要刷新這個item,而且將上一個選中的item的Widget進行刷新。因此這裏定義了一個lastClickItemState變量來保存上一個點擊的item的State對象,每次點擊item的時候,調用這個lastClickItemState的refreshItem方法。
ItemContainerState lastClickItemState;//上一個點擊的item
複製代碼
這個相信搞過PageView的朋友,都想切換到其餘頁面的時候,須要實現頁面保持狀態。 AutomaticKeepAliveClientMixin 這個 Mixin 是 Flutter 爲了保持頁面設置的。哪一個頁面須要保持頁面狀態,就在這個頁面進行混入。
只有兩個組件才能保持頁面狀態:PageView 和 IndexedStack。
因此這裏利用AutomaticKeepAliveClientMixin實現切換月份的時候,會維持上一個月或者下一個月的頁面。
加入 AutomaticKeepAliveClientMixin 混入,並重寫 wantKeepAlive 方法。
周視圖和月視圖的切換功能的實現,這個暫時是用AnimatedContainer+IndexStack來實現的。不清楚還有沒有其餘更好的實現方案,還請大佬們給給意見。
一開始是用Stack來放周視圖和月視圖兩個widget,也是能夠實現效果的。後面看到有IndexStack這個東西,就拿來使用了。
與Stack相同的地方是都是實現層疊的佈局,與Stack不一樣的地方是Stack顯示的時候會把children都繪製出來。而IndexedStack只會繪製指定index的那個child。因此這裏利用切換index來切換顯示周視圖和月視圖。
Stack對應的renderObject是RenderStack,能夠看到,paint方法,最後是會將全部的child的給繪製出來。
IndexedStack實際上是繼承Stack,相應的renderObject是RenderIndexedStack,也是繼承於RenderStack。重寫了paintStack方法,只會繪製指定index的child。
本身寫一個開源庫,雖然寫得很辣雞,不過仍是有挺多收穫的。經過這個項目,把Flutter性能優化的方法進行了實踐,更深刻地去了解Flutter的一些原理。