Flutter日曆專案的優化記錄

入魔的冬瓜 發表於 2019-10-15

FlutterCalendarWidget

Flutter上的一個日曆控制元件,可以定製成自己想要的樣子。

實現日曆並不難,主要是之前實現的效能有點差,進行了下優化。這篇文章主要記錄一下自己對這個日曆專案的優化過程。

截圖

專案地址

日曆支援web預覽:點選此處進入預覽

Flutter日曆專案的優化記錄 Flutter日曆專案的優化記錄

專案結構

下圖就是專案的整體結構,沒啥特殊的,就是寫一個日曆需要的一些資料。

  • constants:存放一些常量
  • model:自定義日曆用的實體類DateModel
  • style:自定義的一些style
  • utils:工具類
  • widget:顯示月檢視、周檢視的widget。
  • calendar_provider:使用provider建立的共享狀態類
  • configuration:定義配置資訊
  • controller:日曆控制器,可以對日曆進行一些操作或者配置
  • flutter_custom_calendar: 日曆Widget

Flutter日曆專案的優化記錄

Widget層級

  • 整體是一個Column,頂部固定一個自定義的weekbar。
  • 下面是一個AnimatedContainer,用來實現周檢視和月檢視切換的高度變化的動畫效果。
  • IndexedStack用來存放周檢視和月檢視的widget。與Stack相同的地方是都是實現層疊的佈局,與Stack不同的地方是Stack顯示的時候會把children都繪製出來。而IndexedStack只會繪製指定index的那個child。所以這裡利用切換index來切換顯示周檢視和月檢視。具體文章:Exploring Stack and IndexedStack in Flutter
    Flutter日曆專案的優化記錄

優化記錄

優化後的效能

優化前,真tm是一片紅。

進行各種優化後,最終的效能如下面幾個圖所示。整體效能還好,頁面間切換也沒那麼卡來。每幀的耗時大部分小於16ms,達到期望,還可以繼續優化下。

  • 圖一:AndroidStudio自帶的工具欄Flutter Performance,可以檢視相關的效能資料
  • 圖二:開啟Show Performance Overlay的開關,就可以在app上顯示效能統計資料的浮窗。
  • 圖三:Flutter提供的Devtools工具,在瀏覽器中可以檢視各種效能的資料。(具體使用百度一下)
    Flutter日曆專案的優化記錄
    Flutter日曆專案的優化記錄
    Flutter日曆專案的優化記錄

程式碼優化

之前日曆的配置資訊全都是放在controller裡面的,而且日曆可配置的資訊會有很多,感覺有點亂。這次將配置的資訊都抽放到一個CalendarConfiguration物件中。並且這個CalendarConfiguration物件存放在頂層的provider狀態類中,可以讓子Widget直接獲取到配置資訊。

Flutter日曆專案的優化記錄

引進provider狀態管理框架

引進provider狀態管理框架,一方面是可以避免資料各種巢狀傳遞,一方面是實現區域性重新整理提高效能。

關於provider的使用:Flutter | 狀態管理指南篇——Provider

  • 引進provider前的程式碼

各種資料和狀態都需要一層一層往下傳遞給子Widget,有點噁心。

Flutter日曆專案的優化記錄
Flutter日曆專案的優化記錄

  • 引進provider後的程式碼

建立狀態類CalendarProvider,用於共享日曆的資料和狀態,子widget可以直接獲取到CalendarProvider中的資料。程式碼比之前好看一點,不過後面還是得繼續優化。

Flutter日曆專案的優化記錄
Flutter日曆專案的優化記錄
沒有了程式碼巢狀,構造方法簡單了很多。在子元件的build方法裡,也可以獲取到各種狀態資料。
Flutter日曆專案的優化記錄

使用compute載入資料

相關文章:Flutter 非同步程式設計:Future、Isolate 和事件迴圈

Flutter日曆專案的優化記錄

  • 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中進行並行執行。

    • JSON 解碼:解碼 JSON(HttpRequest 的響應)可能需要一些時間 => 使用 compute
    • 加密:加密可能非常耗時 => Isolate
    • 影象處理:處理影象(比如:剪裁)確實需要一些時間來完成 => Isolate
  • 在日曆專案中的應用

比如我準備顯示月檢視,需要進行資料的載入拿到42個item。就需要去計算這個月對應的是42個item,以及去計算出每個item所對應的DateModel,以及各種所需要的資訊。這個過程是比較耗時的,所以我就使用compute將這個操作放在另一個isolate裡面進行操作。

Flutter日曆專案的優化記錄

使用getter實現資料的懶載入

Dart中的某個變數,預設都有setter和getter方法,getter方法就是用來獲取變數的值。

如果這個變數的值是需要一系列計算(可能比較耗時)後才能得到結果。 如果在建立這個物件的時候就去計算結果並賦值給這個變數,就會花費額外的一些時間。也有可能計算了,但是這個物件後面也不會用到,那就很沒必要了。

所以可以使用類似下面的寫法,實現類似懶載入的功能。呼叫getter方法的時候,才去判斷值是否為空,為空的話才開始進行資料計算。

Flutter日曆專案的優化記錄

每個DateModel都包含了一大堆屬性,需要我去計算,比如農曆、傳統節日、24節氣。這些的計算是比較複雜和耗時的,所以我就將部分屬性的計算操作放到對應getter方法中,這樣的話,就不用在一開始載入資料的時候,將所有的屬性都進行計算。

Flutter日曆專案的優化記錄

點選item後進行重新整理日曆

呵呵,之前點選item後,然後不管三七二之一,呼叫setState方法去重新整理整個日曆的狀態,搞定。可想而知,效能肯定會差點,所以這裡的優化是,儘量去重新整理的較少數量的item。

Flutter日曆專案的優化記錄

提高Build效率的一種方法就是降低遍歷的出發點。直接在日曆Widget內呼叫它的setState的話,那rebuild的時候,就需要將整個日曆Widget樹進行遍歷重新整理。

所以這裡的做法是將日曆的item抽成一個StatefulWidget,這樣的話,如果呼叫日曆item的setState方法的話,就只會重新整理這個item,實現將重新整理範圍縮小到item級別。

Flutter日曆專案的優化記錄

  • 這裡寫了一個refreshItem的方法,可以給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就行了。

多選模式就很簡單,每個item,都利用GestureDetector監聽日曆item的點選事件,然後呼叫setState方法重新整理自身就行。

Flutter日曆專案的優化記錄

  • 單選模式:只重新整理兩個item,當前item和上一個item。

單選模式,比如我們選中某一個item,需要重新整理這個item,並且將上一個選中的item的Widget進行重新整理。所以這裡定義了一個lastClickItemState變數來儲存上一個點選的item的State物件,每次點選item的時候,呼叫這個lastClickItemState的refreshItem方法。

  ItemContainerState lastClickItemState;//上一個點選的item
複製程式碼

使用AutomaticKeepAliveClientMixin,使PageView儲存內部item狀態

這個相信搞過PageView的朋友,都想切換到其他頁面的時候,需要實現頁面保持狀態。 AutomaticKeepAliveClientMixin 這個 Mixin 是 Flutter 為了保持頁面設定的。哪個頁面需要保持頁面狀態,就在這個頁面進行混入。

只有兩個元件才能保持頁面狀態:PageView 和 IndexedStack。

所以這裡利用AutomaticKeepAliveClientMixin實現切換月份的時候,會維持上一個月或者下一個月的頁面。

加入 AutomaticKeepAliveClientMixin 混入,並重寫 wantKeepAlive 方法。

Flutter日曆專案的優化記錄
Flutter日曆專案的優化記錄

使用IndexStack實現切換功能:

周檢視和月檢視的切換功能的實現,這個暫時是用AnimatedContainer+IndexStack來實現的。不清楚還有沒有其他更好的實現方案,還請大佬們給給意見。

  • 一個是動畫效果。兩個檢視之間的切換,高度變化會有個動畫效果,可以使用現成的AnimatedContainer來實現。比自己用AnimationController會方便很多。
  • 用什麼widget來放周檢視和月檢視這兩個Widget,現在是用IndexStack。

一開始是用Stack來放周檢視和月檢視兩個widget,也是可以實現效果的。後面看到有IndexStack這個東西,就拿來使用了。

與Stack相同的地方是都是實現層疊的佈局,與Stack不同的地方是Stack顯示的時候會把children都繪製出來。而IndexedStack只會繪製指定index的那個child。所以這裡利用切換index來切換顯示周檢視和月檢視。

Stack對應的renderObject是RenderStack,可以看到,paint方法,最後是會將所有的child的給繪製出來。

Flutter日曆專案的優化記錄
Flutter日曆專案的優化記錄

IndexedStack其實是繼承Stack,相應的renderObject是RenderIndexedStack,也是繼承於RenderStack。重寫了paintStack方法,只會繪製指定index的child。

Flutter日曆專案的優化記錄

總結

自己寫一個開源庫,雖然寫得很辣雞,不過還是有挺多收穫的。通過這個專案,把Flutter效能優化的方法進行了實踐,更深入地去了解Flutter的一些原理。