跳至主要內容

深入了解 Flutter

本文說明 Flutter 工具組的內部運作方式,使其 API 成為可能。由於 Flutter widget 是使用積極的組合方式建構,因此使用 Flutter 建構的使用者介面具有大量的 widget。為了支援此工作負載,Flutter 使用次線性演算法進行佈局和建構 widget,以及使樹狀結構手術有效率且具有許多常數因子優化的資料結構。透過一些額外的細節,此設計也讓開發人員可以使用回呼輕鬆建立無限滾動列表,這些回呼只會建構對使用者可見的 widget。

積極的組合性

#

Flutter 最獨特的方面之一是其積極的組合性。Widget 是透過組合其他 widget 來建構的,而這些 widget 本身又是從逐漸更基本的 widget 建構而成的。例如,Padding 是一個 widget,而不是其他 widget 的屬性。因此,使用 Flutter 建構的使用者介面由許多、許多 widget 組成。

widget 建構遞迴的終點在 RenderObjectWidgets 中,這些 widget 會在底層的渲染樹中建立節點。渲染樹是一個資料結構,它儲存使用者介面的幾何形狀,這些幾何形狀是在佈局期間計算的,並在繪製點擊測試期間使用。大多數 Flutter 開發人員不會直接編寫渲染物件,而是使用 widget 操作渲染樹。

為了在 widget 層支援積極的組合性,Flutter 在 widget 和渲染樹層都使用許多有效率的演算法和最佳化,這些演算法和最佳化在以下小節中進行說明。

次線性佈局

#

對於大量的 widget 和渲染物件來說,良好效能的關鍵在於有效率的演算法。最重要的就是佈局的效能,佈局是一種演算法,用於決定渲染物件的幾何形狀(例如,大小和位置)。某些其他工具組使用 O(N²) 或更差的佈局演算法(例如,在某些約束領域中的定點迭代)。Flutter 的目標是在初始佈局時達到線性效能,而在隨後更新現有佈局的常見情況下達到次線性佈局效能。通常,佈局中花費的時間量應比渲染物件的數量增長得慢。

Flutter 每個影格執行一次佈局,而佈局演算法以單次傳遞的方式運作。約束會由父物件向下傳遞到樹狀結構,方式是父物件在每個子物件上呼叫佈局方法。子物件會遞迴地執行自己的佈局,然後透過從其佈局方法返回來將幾何形狀返回到樹狀結構中。重要的是,一旦渲染物件從其佈局方法返回,該渲染物件將不會再次被訪問[1],直到下一個影格的佈局為止。這種方法將原本可能獨立的測量和佈局傳遞合併為單次傳遞,因此,在佈局期間,每個渲染物件最多會被訪問兩次[2]:一次是在向下傳遞到樹狀結構時,另一次是在向上傳遞到樹狀結構時。

Flutter 具有此通用協定的一些特殊化版本。最常見的特殊化版本是 RenderBox,它在二維笛卡爾座標中運作。在盒狀佈局中,約束是最小和最大寬度以及最小和最大高度。在佈局期間,子物件透過在這些邊界內選擇大小來決定其幾何形狀。在子物件從佈局返回後,父物件會決定子物件在父物件座標系統中的位置[3]。請注意,子物件的佈局不能依賴於其位置,因為位置直到子物件從佈局返回後才決定。因此,父物件可以自由地重新放置子物件,而無需重新計算其佈局。

更一般而言,在佈局期間,唯一從父物件流向子物件的資訊是約束,而唯一從子物件流向父物件的資訊是幾何形狀。這些不變性可以減少佈局期間所需的工作量

  • 如果子物件尚未將其自己的佈局標記為髒,則只要父物件提供給子物件的約束與子物件在先前佈局期間接收到的約束相同,則子物件就可以立即從佈局返回,從而切斷遍歷。

  • 每當父物件呼叫子物件的佈局方法時,父物件會指示它是否使用從子物件返回的大小資訊。如果像經常發生的情況一樣,父物件不使用大小資訊,則如果子物件選擇新的大小,則父物件無需重新計算其佈局,因為可以保證新的大小符合現有的約束。

  • 緊密約束是那些可以透過一個有效幾何形狀完全滿足的約束。例如,如果最小和最大寬度彼此相等,且最小和最大高度彼此相等,則唯一滿足這些約束的大小就是具有該寬度和高度的大小。如果父物件提供緊密約束,則只要子物件重新計算其佈局,父物件就不需要重新計算其佈局,即使父物件在其佈局中使用子物件的大小,因為子物件在沒有來自其父物件的新約束的情況下無法變更大小。

  • 渲染物件可以宣告它僅使用父物件提供的約束來決定其幾何形狀。這種宣告會通知框架,當子物件重新計算其佈局時,該渲染物件的父物件不需要重新計算其佈局,即使約束不緊密,並且即使父物件的佈局取決於子物件的大小,因為子物件在沒有來自其父物件的新約束的情況下無法變更大小。

由於這些最佳化,當渲染物件樹包含髒節點時,在佈局期間只會訪問這些節點以及其周圍的有限子樹部分。

次線性 widget 建構

#

與佈局演算法類似,Flutter 的 widget 建構演算法也是次線性的。在建構後,widget 由元素樹保存,該樹保留使用者介面的邏輯結構。元素樹是必要的,因為 widget 本身是不可變的,這表示(除其他事項外),它們無法記住它們與其他 widget 的父子關係。元素樹也保留與有狀態 widget 相關聯的狀態物件。

為了回應使用者輸入(或其他刺激),元素可能會變髒,例如,如果開發人員在關聯的狀態物件上呼叫 setState()。框架會保留一份髒元素清單,並在建構階段直接跳到它們,跳過乾淨的元素。在建構階段,資訊會單向向下流動到元素樹中,這表示在建構階段,每個元素最多會被訪問一次。一旦清理完畢,元素就無法再次變髒,因為根據歸納,其所有祖先元素也都乾淨[4]

因為 widget 是不可變的,所以如果元素尚未將自己標記為髒,則如果父物件使用相同的 widget 重新建構元素,則元素可以立即從建構返回,從而切斷遍歷。此外,元素只需要比較兩個 widget 參考的物件身分,即可確定新 widget 與舊 widget 相同。開發人員會利用此最佳化來實作重新投影模式,在此模式中,widget 會將預先建構的子 widget 作為成員變數包含在其建構中。

在建構期間,Flutter 還會避免使用 InheritedWidgets 遍歷父鏈。如果 widget 通常遍歷其父鏈,例如為了決定目前的主題顏色,則建構階段會在樹的深度中變成 O(N²),由於積極的組合性,樹的深度可能相當大。為了避免這些父項遍歷,框架會向下推送資訊到元素樹,方式是在每個元素維護一個 InheritedWidget 的雜湊表。通常,許多元素會參考相同的雜湊表,該雜湊表只會在引入新 InheritedWidget 的元素處變更。

線性協調

#

與普遍的看法相反,Flutter 不使用樹狀結構差異演算法。相反,框架會透過使用 O(N) 演算法獨立檢查每個元素的子清單,來決定是否重複使用元素。子清單協調演算法針對以下情況進行最佳化

  • 舊的子清單為空。
  • 兩個清單相同。
  • 在清單中的一個位置恰好插入或移除一個或多個 widget。
  • 如果每個清單都包含一個具有相同鍵[5]的 widget,則會比對這兩個 widget。

一般方法是透過比較每個 widget 的執行階段類型和鍵來比對兩個子清單的開頭和結尾,可能會在每個清單的中間找到一個非空範圍,該範圍包含所有不相符的子項。然後,框架會根據子清單中它們的鍵,將舊的子清單中範圍內的子項放入雜湊表中。接下來,框架會遍歷新的子清單中的範圍,並依鍵查詢雜湊表以尋找比對項。不相符的子項會被捨棄並從頭開始重新建構,而相符的子項會使用其新的 widget 重新建構。

樹狀結構手術

#

重複使用元素對於效能很重要,因為元素擁有兩個關鍵資料:有狀態 widget 的狀態和底層的渲染物件。當框架能夠重複使用元素時,會保留使用者介面的邏輯部分的狀態,並且可以重複使用先前計算的佈局資訊,通常可以避免整個子樹遍歷。事實上,重複使用元素非常重要,Flutter 支援非本地樹狀結構突變,以保留狀態和佈局資訊。

開發人員可以透過將 GlobalKey 與其 widget 之一建立關聯來執行非本地樹狀結構突變。每個全域鍵在整個應用程式中都是唯一的,並在執行緒特定的雜湊表中註冊。在建構階段,開發人員可以使用全域鍵將 widget 移動到元素樹中的任意位置。框架會檢查雜湊表,並將現有的元素從其先前的位置重新置於其新位置,而不是在該位置建構一個新的元素,從而保留整個子樹。

重新置於父項的子樹中的渲染物件能夠保留其佈局資訊,因為佈局約束是唯一在渲染樹中從父物件流向子物件的資訊。新父項會被標記為佈局髒,因為其子清單已變更,但如果新父項傳遞給子物件的佈局約束與子物件從其舊父項接收到的佈局約束相同,則子物件可以立即從佈局返回,從而切斷遍歷。

開發人員廣泛使用全域鍵和非本地樹狀結構突變來實現英雄過場動畫和導覽等效果。

常數因子優化

#

除了這些演算法上的最佳化之外,要實現高度的組合性也仰賴於幾個重要的常數因子最佳化。這些最佳化在上述主要演算法的葉節點上最為重要。

  • 與子模型無關。 與大多數使用子列表的工具包不同,Flutter 的渲染樹並未限定特定的子模型。例如,RenderBox 類別有一個抽象的 visitChildren() 方法,而不是具體的 firstChildnextSibling 介面。許多子類別僅支援單一子節點,並直接作為成員變數持有,而不是子節點列表。例如,RenderPadding 僅支援單一子節點,因此具有更簡單的佈局方法,執行時間更短。

  • 視覺渲染樹,邏輯 Widget 樹。 在 Flutter 中,渲染樹在裝置獨立的視覺座標系統中運作,這表示 x 座標中的較小值始終朝向左側,即使目前的閱讀方向是從右到左。Widget 樹通常在邏輯座標中運作,表示具有起始結束值,其視覺解釋取決於閱讀方向。從邏輯座標到視覺座標的轉換是在 Widget 樹和渲染樹之間的交接處完成。這種方法更有效率,因為渲染樹中的佈局和繪製計算比 Widget 到渲染樹的交接更頻繁,並且可以避免重複的座標轉換。

  • 文字由專用的渲染物件處理。 絕大多數渲染物件都不瞭解文字的複雜性。相反,文字是由一個專用的渲染物件 RenderParagraph 處理,它是渲染樹中的葉節點。開發人員不是子類化一個感知文字的渲染物件,而是使用組合將文字整合到其使用者介面中。這種模式表示,只要其父節點提供相同的佈局約束,RenderParagraph 就可以避免重新計算其文字佈局,這很常見,即使在樹狀結構變動期間也是如此。

  • 可觀察物件。 Flutter 同時使用模型觀察和反應式範式。顯然,反應式範式佔主導地位,但 Flutter 對於某些葉節點資料結構使用可觀察的模型物件。例如,Animation 的值變更時會通知觀察者列表。Flutter 將這些可觀察的物件從 Widget 樹交給渲染樹,渲染樹會直接觀察它們,並且僅在它們變更時使管線的適當階段失效。例如,對 Animation<Color> 的變更可能僅觸發繪製階段,而不是建構和繪製階段。

總體而言,並且將這些最佳化加總在由高度組合建立的大型樹狀結構上,它們對效能產生顯著影響。

Element 和 RenderObject 樹的分離

#

Flutter 中的 RenderObjectElement(Widget)樹是同構的(嚴格來說,RenderObject 樹是 Element 樹的子集)。一個顯而易見的簡化方法是將這些樹合併為一個樹。然而,實際上,將這些樹分開有許多好處

  • 效能。 當佈局變更時,只需要遍歷佈局樹的相關部分。由於組合,元素樹經常有許多額外的節點,這些節點必須跳過。

  • 清晰度。 更清晰的關注點分離讓 Widget 協定和渲染物件協定可以各自專注於其特定需求,簡化 API 介面,從而降低錯誤風險和測試負擔。

  • 類型安全。 渲染物件樹可以更具類型安全性,因為它可以保證子節點在執行時將會是適當的類型(例如,每個座標系統都有其自己的渲染物件類型)。組合 Widget 可以不了解佈局期間使用的座標系統(例如,在應用程式模型中公開一部分的相同 Widget 可以用於方塊佈局和 Sliver 佈局),因此在元素樹中,驗證渲染物件的類型將需要遍歷樹狀結構。

無限滾動

#

無限滾動清單對工具包來說出了名的難處理。Flutter 使用基於建構器模式的簡單介面來支援無限滾動清單,其中 ListView 使用回呼函式,以便在滾動期間使用者可見時按需建構 Widget。支援此功能需要視窗感知佈局按需建構 Widget

視口感知佈局

#

與 Flutter 中的大多數事物一樣,可滾動 Widget 是使用組合來建構的。可滾動 Widget 的外部是一個 Viewport,它是一個「內部較大」的方塊,這表示其子節點可以延伸到視窗的邊界之外,並且可以滾動到檢視中。然而,視窗不是擁有 RenderBox 子節點,而是擁有 RenderSliver 子節點,這些子節點稱為 Sliver,它們具有視窗感知佈局協定。

Sliver 佈局協定與方塊佈局協定的結構相符,父節點將約束傳遞給子節點,並接收回幾何圖形。然而,兩個協定之間的約束和幾何資料不同。在 Sliver 協定中,子節點會被提供關於視窗的資訊,包括剩餘可見空間量。它們傳回的幾何資料可以實現各種與滾動連結的效果,包括可摺疊標頭和視差。

不同的 Sliver 以不同的方式填滿視窗中可用的空間。例如,產生子節點線性清單的 Sliver 會依序佈局每個子節點,直到 Sliver 用完子節點或用完空間。類似地,產生子節點二維網格的 Sliver 只會填滿網格中可見的部分。由於它們知道有多少空間是可見的,Sliver 即使有可能產生無限數量的子節點,也可以產生有限數量的子節點。

Sliver 可以組合以建立自訂的可滾動佈局和效果。例如,單一視窗可以具有可摺疊標頭,然後是線性清單,然後是網格。這三個 Sliver 將透過 Sliver 佈局協定來合作,以便只產生那些實際上透過視窗可見的子節點,無論這些子節點屬於標頭、清單還是網格[6]

依需求建構 widget

#

如果 Flutter 具有嚴格的建構,然後佈局,然後繪製管線,則以上方法不足以實作無限滾動清單,因為關於有多少空間透過視窗可見的資訊僅在佈局階段可用。如果沒有額外的機制,佈局階段對於建構填滿空間所需的 Widget 來說太晚了。Flutter 透過交錯管線的建構和佈局階段來解決此問題。在佈局階段的任何時間點,框架都可以按需開始建構新的 Widget,只要這些 Widget 是目前執行佈局的渲染物件的後代

只有在對建構和佈局演算法中的資訊傳播進行嚴格控制的情況下,才有可能交錯建構和佈局。具體來說,在建構階段,資訊只能向下傳播到樹狀結構中。當渲染物件執行佈局時,佈局遍歷尚未造訪該渲染物件下方的子樹,這表示在該子樹中建構產生的寫入無法使至今已進入佈局計算的任何資訊失效。同樣地,一旦佈局從渲染物件傳回,就不會在本次佈局期間再次造訪該渲染物件,這表示後續佈局計算產生的任何寫入都無法使先前用於建構渲染物件子樹的資訊失效。

此外,線性調解和樹狀結構變動對於在滾動期間有效率地更新元素,以及當元素在視窗邊緣滾動進入和移出檢視時修改渲染樹至關重要。

API 人體工學

#

只有當框架實際上可以有效使用時,快速才有意義。為了引導 Flutter 的 API 設計走向更高的可用性,Flutter 已在與開發人員進行的大量 UX 研究中重複測試。這些研究有時證實了先前存在的設計決策,有時有助於引導功能的優先順序,有時則改變了 API 設計的方向。例如,Flutter 的 API 包含大量文件;UX 研究證實了此類文件的價值,但也強調了對範例程式碼和說明圖的需求。

本節討論為了提高可用性而在 Flutter 的 API 設計中做出的一些決策。

客製化 API 以符合開發人員的思維模式

#

Flutter 的 WidgetElementRenderObject 樹中節點的基底類別未定義子模型。這讓每個節點都能針對適用於該節點的子模型進行客製化。

大多數 Widget 物件都有單一子 Widget,因此只公開單一 child 參數。有些 Widget 支援任意數量的子節點,並公開接受列表的 children 參數。有些 Widget 根本沒有子節點,並且不保留記憶體,也沒有子節點的參數。類似地,RenderObject 公開特定於其子模型的 API。RenderImage 是一個葉節點,沒有子節點的概念。RenderPadding 採用單一子節點,因此它具有單一指標的儲存空間,指向單一子節點。RenderFlex 採用任意數量的子節點,並將其作為連結清單來管理。

在某些少數情況下,會使用更複雜的子模型。RenderTable 渲染物件的建構子會採用子節點的陣列,類別會公開控制列數和欄數的 getter 和 setter,並且有特定的方法可以按 x,y 座標取代個別子節點,新增列,提供子節點的新陣列,以及將整個子節點列表取代為單一陣列和欄數。在實作中,該物件不像大多數渲染物件那樣使用連結清單,而是使用可索引的陣列。

Chip Widget 和 InputDecoration 物件具有與相關控制項上存在的槽對應的欄位。例如,在一個通用子模型會強制語義層疊在子節點列表之上(例如,將第一個子節點定義為前綴值,將第二個子節點定義為後綴)的情況下,專用的子模型允許改用專用的具名屬性。

這種彈性讓這些樹中的每個節點都能以最適合其角色的方式操作。很少會想要在表格中插入儲存格,導致所有其他儲存格換行;類似地,很少會想要依索引而不是依參考從彈性列中移除子節點。

RenderParagraph 物件是最極端的情況:它有一個完全不同類型的子節點 TextSpan。在 RenderParagraph 邊界,RenderObject 樹轉換為 TextSpan 樹。

專門化 API 以符合開發人員期望的整體方法適用於子模型以外的其他事物。

有些相當微不足道的 Widget 存在,正是為了讓開發人員在尋找問題解決方案時找到它們。只要知道如何使用,使用 Expanded Widget 和零大小的 SizedBox 子節點,就很容易在列或欄中新增空間,但是發現這種模式是不必要的,因為搜尋 space 會發現 Spacer Widget,它直接使用 ExpandedSizedBox 來達到效果。

類似地,隱藏 Widget 子樹很容易透過完全不將 Widget 子樹包含在建構中來完成。然而,開發人員通常期望有一個 Widget 可以執行此操作,因此存在 Visibility Widget 以將此模式包裝在一個微不足道的可重複使用 Widget 中。

明確的參數

#

UI 框架往往有許多屬性,因此開發人員很少能夠記住每個類別的每個建構子引數的語義含義。由於 Flutter 使用反應式範式,因此 Flutter 中的建構方法很常見有多個對建構子的呼叫。藉由利用 Dart 對具名引數的支援,Flutter 的 API 能夠保持此類建構方法的清晰和可理解性。

這個模式延伸適用於任何具有多個參數的方法,特別是延伸適用於任何布林值參數,因此方法呼叫中單獨的 truefalse 常值始終是自我說明的。此外,為了避免 API 中常見的雙重否定造成的混淆,布林值參數和屬性始終以肯定形式命名(例如,enabled: true 而不是 disabled: false)。

鋪平陷阱

#

Flutter 框架中多處使用的一種技術是定義 API,使其不存在錯誤情況。這消除了需要考慮的整類錯誤。

例如,插值函數允許插值的其中一端或兩端為 null,而不是將其定義為錯誤情況:在兩個 null 值之間插值始終為 null,而從 null 值或到 null 值插值則等同於插值到給定類型的零值類比。這表示開發人員不小心將 null 傳遞給插值函數時不會遇到錯誤情況,而是會得到合理的結果。

一個更細微的例子是 Flex 版面配置演算法。此版面配置的概念是將給予 flex 渲染物件的空間分配給其子物件,因此 flex 的大小應該是可用空間的全部。在最初的設計中,提供無限空間會失敗:這表示 flex 應該是無限大小,這是一個無用的版面配置組態。相反地,調整了 API,使得當無限空間分配給 flex 渲染物件時,渲染物件會調整自身大小以符合子物件的所需大小,從而減少了可能的錯誤情況數量。

這種方法也用於避免創建允許不一致資料的建構函式。例如,PointerDownEvent 建構函式不允許將 PointerEventdown 屬性設定為 false(這是一種自相矛盾的情況);相反地,建構函式沒有 down 欄位的參數,並且始終將其設定為 true

一般而言,這種方法是為輸入域中的所有值定義有效的解釋。最簡單的例子是 Color 建構函式。預設建構函式不是採用四個整數(紅色、綠色、藍色和 Alpha 各一個),每個整數都可能超出範圍,而是採用單個整數值,並定義每個位元的含義(例如,最低的八個位元定義紅色成分),以便任何輸入值都是有效的顏色值。

一個更複雜的例子是 paintImage() 函數。這個函數有 11 個參數,其中一些參數具有相當寬的輸入域,但它們經過仔細設計,使其彼此大部分正交,因此幾乎沒有無效的組合。

積極回報錯誤情況

#

並非所有錯誤情況都可以被設計出來。對於剩餘的錯誤情況,在偵錯版本中,Flutter 通常會嘗試盡早捕獲錯誤並立即報告。廣泛使用了斷言。建構函式參數會進行詳細的健全性檢查。監控生命週期,當檢測到不一致時,會立即引發例外狀況。

在某些情況下,這會被推到極致:例如,在執行單元測試時,無論測試還在做什麼,每個已佈局的 RenderBox 子類都會積極檢查其本質大小調整方法是否符合本質大小調整合約。這有助於捕獲可能不會被執行的 API 中的錯誤。

當拋出例外狀況時,它們會包含盡可能多的資訊。Flutter 的某些錯誤訊息會主動探查相關的堆疊追蹤,以確定實際錯誤最可能的位置。其他錯誤訊息會遍歷相關的樹狀結構,以確定錯誤資料的來源。最常見的錯誤包含詳細的說明,其中有些情況還包含避免錯誤的範例程式碼,或連結到更多文件。

反應式範式

#

基於可變樹狀結構的 API 會遇到二分存取模式:建立樹狀結構的原始狀態通常會使用與後續更新非常不同的操作集。Flutter 的渲染層使用此範例,因為它是維護持久樹狀結構的有效方法,這對於高效的版面配置和繪製至關重要。但是,這表示直接與渲染層互動充其量是笨拙的,最壞的情況是容易出錯的。

Flutter 的 widget 層引入了一種使用反應式範例[7]來操作底層渲染樹狀結構的組合機制。此 API 通過將樹狀結構建立和樹狀結構變更步驟合併為單個樹狀結構描述(建立)步驟來抽象化樹狀結構操作,其中,在每次系統狀態變更後,由開發人員描述使用者介面的新組態,而框架計算反映此新組態所需的樹狀結構變更系列。

插值

#

由於 Flutter 的框架鼓勵開發人員描述符合當前應用程式狀態的介面組態,因此存在一種機制可以在這些組態之間隱式建立動畫效果。

例如,假設在狀態 S1 中,介面由一個圓形組成,但在狀態 S2 中,它由一個正方形組成。如果沒有動畫機制,狀態變更將會導致介面發生突兀的變化。隱式動畫允許圓形在多個影格上平滑地變成正方形。

每個可以隱式建立動畫效果的功能都有一個狀態 widget,它會記錄輸入的目前值,並在輸入值變更時啟動動畫序列,在指定的持續時間內從目前值轉換到新值。

這是使用 lerp(線性插值)函數搭配不可變物件實作的。每個狀態(在此例中為圓形和正方形)都表示為一個不可變物件,該物件使用適當的設定(顏色、筆劃寬度等)配置,並且知道如何繪製自身。當需要繪製動畫期間的中間步驟時,會將開始值和結束值以及代表動畫中某一點的 *t* 值傳遞給適當的 lerp 函數,其中 0.0 代表 start,而 1.0 代表 end[8],而且該函數會傳回代表中間階段的第三個不可變物件。

對於從圓形到正方形的轉換,lerp 函數會傳回一個代表「圓角正方形」的物件,其半徑描述為從 *t* 值導出的分數、使用顏色 lerp 函數內插的顏色,以及使用雙精度浮點數 lerp 函數內插的筆劃寬度。該物件實作與圓形和正方形相同的介面,因此當要求繪製時,便能夠繪製自身。

這項技術允許狀態機制、狀態到組態的映射、動畫機制、插值機制以及與如何繪製每個影格相關的特定邏輯完全彼此分離。

此方法廣泛適用。在 Flutter 中,可以內插 ColorShape 等基本類型,但也可以內插 DecorationTextStyleTheme 等更複雜的類型。這些類型通常由本身可以內插的元件建構而成,而且內插更複雜的物件通常就像遞迴地內插所有描述複雜物件的值一樣簡單。

某些可內插物件是透過類別階層定義的。例如,形狀由 ShapeBorder 介面表示,並且存在各種形狀,包括 BeveledRectangleBorderBoxBorderCircleBorderRoundedRectangleBorderStadiumBorder。單個 lerp 函數無法預測所有可能的類型,因此介面改為定義 lerpFromlerpTo 方法,而靜態 lerp 方法會延遲到這些方法。當被告知要從形狀 A 內插到形狀 B 時,首先會詢問 B 是否可以從 A lerpFrom,然後,如果無法,則改為詢問 A 是否可以 lerpTo B。(如果兩者都不可能,則該函數會從小於 0.5 的 *t* 值傳回 A,否則傳回 B。)

這允許任意擴展類別階層,並且後續新增的功能可以在先前已知的值和它們自身之間進行內插。

在某些情況下,內插本身無法由任何可用的類別描述,並且會定義私有類別來描述中間階段。例如,在 CircleBorderRoundedRectangleBorder 之間內插時就是這種情況。

此機制還有一個額外的優點:它可以處理從中間階段到新值的內插。例如,在從圓形到正方形的轉換過程中,形狀可能會再次變更一半,導致動畫需要內插到三角形。只要三角形類別可以從圓角正方形中間類別 lerpFrom,就可以無縫執行轉換。

結論

#

Flutter 的標語「一切都是 widget」圍繞著透過組合 widget 來建立使用者介面,而 widget 又是由越來越基本的 widget 組成的。這種積極組合的結果是產生大量 widget,這些 widget 需要經過精心設計的演算法和資料結構才能有效率地處理。透過一些額外的設計,這些資料結構也讓開發人員能夠輕鬆建立無限滾動清單,這些清單會在 widget 可見時按需建立 widget。


腳註


  1. 至少對於版面配置而言是如此。如果需要,可以重新檢視其是否適用於繪製、建立協助工具樹狀結構和點擊測試。↩︎

  2. 當然,現實情況更複雜一些。有些版面配置涉及本質尺寸或基準測量,這確實需要額外遍歷相關的子樹(使用積極快取來減輕最壞情況下產生二次方效能的可能性)。但是,這些情況非常罕見。特別是,縮小包裝的常見情況不需要本質尺寸。↩︎

  3. 從技術上講,子物件的位置不是其 RenderBox 幾何結構的一部分,因此在版面配置期間實際上不需要計算。許多渲染物件會將其單個子物件隱式定位在其原點的 0,0 位置,這根本不需要計算或儲存。有些渲染物件會避免計算其子物件的位置,直到最後一刻(例如,在繪製階段期間),以便在它們後續未繪製時完全避免計算。↩︎

  4. 這個規則有一個例外。如按需建立 Widget章節所討論的,某些 Widget可能會因為佈局約束的改變而被重建。如果一個 Widget 在同一個影格中,因無關的原因標記自己為髒 (dirty) 的同時,也受到佈局約束變更的影響,它將會被更新兩次。這種多餘的建構僅限於 Widget 本身,不會影響其子元件。↩︎

  5. Key 是一個可選地與 Widget 相關聯的不透明物件,其相等運算符用於影響協調演算法。↩︎

  6. 為了協助無障礙功能,並讓應用程式在 Widget 建構完成到顯示在螢幕之間有額外的幾毫秒時間,視口會在可見 Widget 前後幾百像素的地方建立(但不繪製)Widget。↩︎

  7. 這種方法最初是由 Facebook 的 React 函式庫普及的。↩︎

  8. 實際上,t 值允許超出 0.0-1.0 的範圍,並且某些曲線會這樣做。例如,「彈性」曲線會為了呈現彈跳效果而短暫地超出範圍。插值邏輯通常可以根據需要推斷出起點或終點。對於某些類型,例如,當插值顏色時,t 值會有效地被限制在 0.0-1.0 的範圍內。↩︎