深入了解 Flutter
從創始工程師的角度,了解 Flutter 的內部運作原理。
本文件說明了 Flutter 工具包的內部運作原理,這些設計使 Flutter 的 API 得以實現。由於 Flutter 元件 (Widget) 採用高度組合(aggressive composition)的方式構建,使用 Flutter 所打造的使用者介面會包含大量的元件。為了支援這樣的工作負載,Flutter 在版面配置(layout)和元件建構上採用了次線性(sublinear)演算法,以及能高效進行樹狀結構操作的資料結構,並且進行了多項常數因子的最佳化(constant-factor optimizations)。透過一些額外的細節,這種設計也讓開發者能夠輕鬆地利用回呼(callback)建立僅顯示於使用者畫面上的元件,從而實現無限滾動清單。
積極的組合性
#
Flutter 最顯著的特點之一就是其 積極的組合性(aggressive composability)。元件是由其他元件組合而成,而這些元件又是由更基礎的元件逐層構建。例如,Padding
是一個元件,而不是其他元件的屬性。因此,使用 Flutter 建構的使用者介面會由非常多的元件組成。
元件建構的遞迴最終會落在 RenderObjectWidgets,這些元件會在底層的 render 樹上建立節點。render 樹是一種資料結構,用來儲存使用者介面的幾何資訊,這些資訊會在
layout 階段計算,並於 painting(繪製)及 hit testing(點擊測試)時使用。大多數 Flutter 開發者不會直接撰寫 render 物件,而是透過元件來操作 render 樹。
為了在元件層支援積極的組合性,Flutter 在元件層與 render 樹層都採用了多種高效的演算法與最佳化,以下各節將分別說明。
次線性版面配置
#當元件與 render 物件數量龐大時,良好效能的關鍵在於高效的演算法。其中最重要的是 layout(版面配置)效能,也就是決定 render 物件幾何資訊(例如尺寸與位置)的演算法。有些工具包採用 O(N²) 或更差的版面配置演算法(例如某些限制條件領域的定點迭代)。Flutter 目標是在初次版面配置時達到線性效能,並在後續更新既有版面配置時,於常見情境下達到 次線性 效能。理想情況下,layout 所花費的時間應該隨 render 物件數量增加而成長得更慢。
Flutter 每一幀只執行一次 layout,且 layout 演算法僅需單一遍歷。constraints(限制條件)會由父物件往下傳遞,父物件會對每個子物件呼叫 layout 方法。子物件會遞迴執行自身的 layout,然後透過從 layout 方法返回的方式,將 geometry(幾何資訊)往上傳回樹狀結構。重要的是,一旦 render 物件從其 layout 方法返回,在該幀的 layout 階段結束前,該 render 物件不會再次被訪問1。這種方式將原本可能分開的測量(measure)與 layout 階段合併為單一遍歷,因此每個 render 物件在 layout 階段 最多只會被訪問兩次2:一次是往下傳遞時,一次是往上傳回時。
Flutter 對這個通用協議有多種專門化。最常見的是 RenderBox,它在二維笛卡兒座標下運作。在 box layout 中,constraints 是最小與最大寬度、最小與最大高度。在 layout 過程中,子物件會在這些範圍內選擇自己的尺寸。當子物件 layout 完成返回後,父物件會決定子物件在父座標系中的位置3。請注意,子物件的 layout 不能依賴於其位置,因為位置要等到子物件 layout 返回後才決定。因此,父物件可以自由地重新定位子物件,而無需重新計算其 layout。
更一般來說,在 layout 過程中,從父到子的 唯一 資訊是 constraints,從子到父的 唯一 資訊是 geometry。這些不變性(invariants)可以減少 layout 所需的工作量:
如果子物件沒有將自身的 layout 標記為 dirty,且父物件給予的 constraints 與上次 layout 相同,則子物件可以立即從 layout 返回,終止遍歷。
-
每當父物件呼叫子物件的 layout 方法時,父物件會指示是否會使用子物件返回的尺寸資訊。如果父物件不需要尺寸資訊(這種情況很常見),那麼即使子物件選擇了新的尺寸,只要新尺寸符合現有 constraints,父物件也無需重新計算 layout。
-
Tight constraints 指的是只有一組有效 geometry 可以滿足的 constraints。例如,若 min 與 max 寬度相等、min 與 max 高度相等,則唯一滿足這些 constraints 的尺寸就是該寬高。如果父物件提供 tight constraints,則即使父物件 layout 依賴於子物件尺寸,當子物件重新計算 layout 時,父物件也無需重新 layout,因為子物件在未獲得新 constraints 前無法改變尺寸。
-
Render 物件可以宣告它僅使用父物件提供的 constraints 來決定自身 geometry。這樣的宣告會通知框架,當子物件重新 layout 時,即使 constraints 不是 tight、即使父物件 layout 依賴於子物件尺寸,父物件也無需重新 layout,因為子物件在未獲得新 constraints 前無法改變尺寸。
這些最佳化的結果是,當 render 物件樹中有 dirty 節點時,只有這些節點及其周圍有限的子樹會在 layout 過程中被訪問。
次線性元件建構
#與 layout 演算法類似,Flutter 的元件建構演算法也是次線性的。元件建構完成後,會由 element 樹 持有,這個樹保留了使用者介面的邏輯結構。element 樹是必要的,因為元件本身是 不可變 的(immutable),這意味著它們無法記住與其他元件的父子關係。element 樹同時也持有與 stateful 元件相關聯的 狀態 物件。
當用戶輸入(或其他刺激)發生時,element 可能會變成 dirty,例如開發者在相關 state 物件上呼叫 setState()。框架會維護一個 dirty element 清單,並在
build 階段直接跳到這些 dirty element,跳過乾淨(clean)的 element。在 build 階段,資訊會 單向 地往下傳遞 element 樹,因此每個 element 在 build 階段最多只會被訪問一次。當 element 清理完畢後,不會再次變 dirty,因為根據歸納法,其所有祖先 element 也都是乾淨的4。
由於元件是 不可變 的,如果 element 沒有將自己標記為 dirty,當父物件用相同的元件重建 element 時,element 可以立即從 build 返回,終止遍歷。此外,element 只需比較兩個元件參考的物件識別(object identity),即可確認新舊元件是否相同。開發者會利用這項最佳化來實現 reprojection 模式,即元件將預先建構好的子元件作為成員變數,並在 build 時直接使用。
在 build 過程中,Flutter 也會避免遍歷父鏈(parent chain),例如使用 InheritedWidgets。如果元件經常遍歷父鏈,例如為了取得當前主題色彩,build 階段的複雜度會因樹的深度而達到 O(N²),而積極組合會讓樹的深度相當大。為避免這種父鏈遍歷,框架會在每個 element 維護一個
InheritedWidget 的雜湊表,將資訊往下推送。通常,許多 element 會參考同一個雜湊表,只有在引入新 InheritedWidget
的 element 處才會變更。
線性對帳
#與常見認知相反,Flutter 並未採用樹狀差異(tree-diffing)演算法。相反地,框架會針對每個 element 獨立檢查其子清單,並使用 O(N) 演算法決定是否重用 element。子清單對帳演算法針對以下情境進行最佳化:
- 舊的子清單為空。
- 兩個清單完全相同。
- 清單中某一處有一個或多個元件插入或移除。
- 如果每個清單都包含具有相同 key 的元件5,則這兩個元件會被配對。
一般做法是,透過比較每個元件的執行階段型別與 key,將兩個子清單的開頭與結尾配對,可能會在中間找到一個非空區間,包含所有未配對的子元件。框架會將舊子清單中該區間的子元件依 key 放入雜湊表,然後遍歷新子清單的區間,根據 key 查詢雜湊表以尋找配對。未配對的子元件會被丟棄並重新建構,而已配對的則會用新元件重建。
樹狀結構手術
#重用 element 對效能至關重要,因為 element 擁有兩項關鍵資料:stateful 元件的狀態以及底層 render 物件。當框架能夠重用 element 時,該邏輯部分的狀態會被保留,先前計算的 layout 資訊也能重複使用,通常能避免整個子樹的遍歷。事實上,重用 element 如此重要,以至於 Flutter 支援 非區域性(non-local)的樹狀結構變動,以保留狀態與 layout 資訊。
開發者可以將 GlobalKey 關聯到自己的元件上,來進行非區域性樹狀結構變動。每個 global key 在整個應用程式中都是唯一的,並會註冊到特定執行緒的雜湊表中。在 build 階段,開發者可以將帶有 global key 的元件移動到 element 樹的任意位置。此時,框架不會在新位置建立新的 element,而是查詢雜湊表,將現有的 element 從原位置重新掛載到新位置,並保留整個子樹。
被重新掛載的子樹中的 render 物件能夠保留其 layout 資訊,因為在 render 樹中,從父到子的唯一資訊是 layout constraints。新的父物件會因子清單變動而被標記為 layout dirty,但如果新父物件給予子物件與舊父物件相同的 layout constraints,子物件可以立即從 layout 返回,終止遍歷。
Global key 與非區域性樹狀結構變動被開發者廣泛用於實現 hero 動畫、頁面導航等效果。
常數因子最佳化
#除了這些演算法層級的最佳化之外,實現積極組合性還仰賴數個重要的常數因子最佳化。這些最佳化在前述主要演算法的葉節點處最為關鍵。
-
不依賴特定子模型。 與大多數工具包採用子清單不同,Flutter 的 render 樹並未綁定特定的子模型。例如,
RenderBox類別有一個抽象的visitChildren()方法,而非具體的firstChild與nextSibling介面。許多子類僅支援單一子物件,直接以成員變數持有,而非子清單。例如,RenderPadding僅支援單一子物件,因此擁有更簡單、執行更快的 layout 方法。 -
視覺 render 樹與邏輯元件樹分離。 在 Flutter 中,render 樹運作於裝置無關的視覺座標系統中,這意味著 x 座標較小的值永遠在左側,即使當前閱讀方向為從右至左。元件樹則通常運作於邏輯座標系統中,使用 start 和 end 值,其視覺解讀取決於閱讀方向。從邏輯座標到視覺座標的轉換發生在元件樹與 render 樹之間的交接處。這種做法更有效率,因為 render 樹中的 layout 與繪製計算比元件到 render 樹的交接發生得更頻繁,能避免重複座標轉換。
-
文字由專門的 render 物件處理。 絕大多數 render 物件不需處理文字的複雜性。文字由專門的 render 物件
RenderParagraph處理,該物件是 render 樹的葉節點。開發者不需繼承支援文字的 render 物件,而是透過組合將文字納入介面。這種模式讓RenderParagraph只要父物件提供相同的 layout constraints,即可避免重新計算文字 layout,這在樹狀結構手術時也很常見。 -
可觀察物件。 Flutter 同時採用 model-observation 與 reactive(反應式)兩種範式。雖然 reactive 為主流,但 Flutter 在某些葉節點資料結構中仍採用可觀察模型物件。例如,
Animation在其值變更時會通知觀察者清單。Flutter 會將這些可觀察物件從元件樹傳遞到 render 樹,render 樹直接觀察它們,並在變更時僅使適當的管線階段失效。例如,Animation<Color>的變更可能只觸發 paint 階段,而不會同時觸發 build 與 paint 階段。
這些最佳化加總起來,對於由積極組合產生的大型樹狀結構,能顯著提升效能。
Element 樹與 RenderObject 樹的分離
#
Flutter 中的 RenderObject 與 Element(Widget)樹是同構的(嚴格來說,RenderObject
樹是 Element 樹的子集)。一個明顯的簡化方式是將這兩棵樹合併為一棵樹。然而,實務上將這兩棵樹分開有多項好處:
效能。 當 layout 發生變化時,只需遍歷 layout 樹的相關部分。由於組合性,element 樹常常包含許多需要略過的額外節點。
清晰性。 明確的職責分離讓元件協定與 render 物件協定都能針對各自需求進行專門化,簡化 API 介面,降低 bug 風險與測試負擔。
-
型別安全。 Render 物件樹能更安全地檢查型別,因為可於執行時保證子物件型別正確(每種座標系統都有自己的 render 物件型別)。組合元件可不關心 layout 時所用的座標系統(例如同一元件可同時用於 box layout 與 sliver layout),因此若要在 element 樹中驗證 render 物件型別,需進行樹狀遍歷。
無限滾動
#
無限滾動清單對工具包來說是出了名的難題。Flutter 以簡單的介面支援無限滾動清單,基於 builder 模式,其中 ListView 使用回呼(callback)在元件滾動至使用者可見範圍時按需建構元件。支援此功能需要
視口感知版面配置(viewport-aware layout)以及 按需建構元件。
視口感知版面配置
#
如同 Flutter 中的大多數事物,可滾動元件也是透過組合來建構的。可滾動元件的外層是 Viewport,這是一個「內部更大」的盒子,也就是說其子元件可以超出視口的邊界,並可被滾動到可見範圍。然而,Viewport
的子元件不是 RenderBox,而是 RenderSliver 子元件,稱為 slivers,它們採用視口感知的版面配置協定。
sliver 版面配置協定與 box 版面配置協定的結構相符,父物件向子物件傳遞 constraints,子物件返回 geometry。然而,兩種協定的 constraints 與 geometry 資料不同。在 sliver 協定中,子物件會獲得關於視口的資訊,包括剩餘可見空間的大小。它們返回的 geometry 資料能夠實現多種與滾動連動的效果,包括可折疊的標頭與視差效果。
不同的 sliver 以不同方式填充視口中的可用空間。例如,產生子元件線性清單的 sliver 會依序對每個子元件進行版面配置,直到 sliver 耗盡子元件或空間為止。同樣地,產生二維網格子元件的 sliver 只填充其網格中可見的部分。由於 sliver 知道有多少空間是可見的,即使它們有潛力產生無限多個子元件,也只會產生有限數量的子元件。
Sliver 可以組合在一起,建立定制的可滾動版面配置與效果。例如,單一視口可以包含一個可折疊的標頭,後跟一個線性清單,再接著一個網格。這三個 sliver 會透過 sliver 版面配置協定協作,只產生實際上透過視口可見的子元件,不論這些子元件屬於標頭、清單還是網格6。
按需建構元件
#如果 Flutter 採用嚴格的 build-then-layout-then-paint 管線,前述方式就不足以實現無限滾動清單,因為關於透過視口可見空間大小的資訊只有在 layout 階段才能取得。若沒有額外的機制,到了 layout 階段才建構填充空間所需的元件就太晚了。Flutter 透過交錯(interleaving)build 與 layout 管線階段來解決這個問題。在 layout 階段的任何時間點,框架都可以按需開始建構新元件,只要這些元件是當前正在執行 layout 的 render 物件的後代。
交錯 build 與 layout 成為可能,是因為 build 和 layout 演算法對資訊傳播有嚴格的控制。具體來說,在 build 階段,資訊只能向下傳遞到樹中。當 render 物件正在執行 layout 時,layout 遍歷尚未訪問該 render 物件下方的子樹,這意味著在該子樹中建構所產生的寫入,不會使已進入 layout 計算的任何資訊失效。同樣地,一旦 layout 從某個 render 物件返回,該 render 物件在此次 layout 中就不會再被訪問,這意味著後續 layout 計算所產生的任何寫入,也不會使用於建構該 render 物件子樹的資訊失效。
此外,線性對帳與樹狀結構手術對於在滾動過程中高效更新 element,以及在 element 於視口邊緣滾入或滾出時修改 render 樹,都是不可或缺的。
API 人因工程
#只有快速是不夠的,框架還必須能夠被有效地使用。為了引導 Flutter 的 API 設計走向更高的可用性,Flutter 已與開發者進行了多次廣泛的 UX 研究測試。這些研究有時確認了既有的設計決策,有時幫助指引功能優先順序,有時也改變了 API 設計的方向。例如,Flutter 的 API 有豐富的文件;UX 研究確認了這類文件的價值,同時也突顯了對範例程式碼與示意圖的特別需求。
本節討論 Flutter API 設計中為提升可用性所做的一些決策。
使 API 專門化以符合開發者的思維模式
#
Flutter 的 Widget、Element 與 RenderObject 樹中的節點基底類別並未定義子模型。這讓每個節點都能針對適用於該節點的子模型進行專門化。
大多數 Widget 物件只有一個子 Widget,因此只公開一個 child 參數。有些元件支援任意數量的子元件,並公開接受清單的
children 參數。有些元件完全沒有子元件,不保留任何記憶體,也沒有對應的參數。同樣地,RenderObjects 公開的 API 也針對其子模型量身訂製。RenderImage
是葉節點,沒有子元件的概念。RenderPadding 接受單一子元件,因此有一個指向單一子元件的指標。RenderFlex 接受任意數量的子元件,並以鏈結串列管理。
在少數情況下,會使用更複雜的子模型。RenderTable render 物件的建構式接受一個子元件陣列的陣列,該類別公開了控制列數與欄數的 getter 與 setter,並有專門的方法可依 x,y 座標替換個別子元件、新增一列、提供新的陣列的陣列,以及用單一陣列和欄數取代整個子清單。在實作上,該物件不像大多數 render 物件使用鏈結串列,而是使用可索引的陣列。
Chip 元件與 InputDecoration 物件的欄位對應於相關控制項上存在的插槽。若採用一體適用的子模型,則需在子清單之上加上語意層,例如定義第一個子元件為前綴值、第二個為後綴,而專用的子模型則允許使用專用的具名屬性。
這種彈性讓這些樹中的每個節點都能以最符合其角色的方式進行操作。想在表格中插入一個儲存格使其他儲存格環繞的情況很少見;同樣地,想依索引而非依參考從彈性列中移除子元件的情況也很少見。
RenderParagraph 物件是最極端的案例:它有一個完全不同型別的子物件,TextSpan。在 RenderParagraph
邊界處,RenderObject 樹過渡為 TextSpan 樹。
使 API 專門化以符合開發者期望的整體做法,不僅限於子模型。
某些相當微不足道的元件的存在,純粹是為了讓開發者在尋找解決方案時能找到它們。在列或行中加入間距,一旦知道方法便很容易做到,只需使用 Expanded 元件和一個零尺寸的 SizedBox
子元件,但發現這個模式並非必要,因為搜尋 space 就能找到 Spacer 元件,它直接使用 Expanded 和 SizedBox
來達到效果。
同樣地,隱藏元件子樹可以輕鬆地藉由在 build 時不包含該子樹來實現。然而,開發者通常期望有一個元件來做這件事,因此 Visibility 元件的存在就是為了將這種模式包裝成一個微不足道的可重用元件。
明確的引數
#UI 框架往往有許多屬性,開發者很少能記住每個類別的每個建構式引數的語意含義。由於 Flutter 採用 reactive 範式,Flutter 的 build 方法中常有許多建構式呼叫。透過利用 Dart 對具名引數的支援,Flutter 的 API 能夠讓這類 build 方法保持清晰易懂。
這種模式延伸到任何具有多個引數的方法,特別是延伸到任何布林引數,使方法呼叫中獨立的 true 或 false 字面值永遠是自文件化的。此外,為了避免 API 中雙重否定常帶來的混淆,布林引數和屬性始終以正面形式命名(例如,enabled: true
而非 disabled: false)。
填平陷阱
#Flutter 框架中多個地方使用的一種技術是定義 API 使得錯誤條件不存在。這從根本上消除了整類錯誤。
例如,插值函式允許插值的一端或兩端為 null,而不是將此定義為錯誤情況:在兩個 null 值之間插值始終返回 null,從 null 值插值或插值到 null 值相當於插值到該型別的零類比。這意味著開發者不小心將 null 傳入插值函式時,不會遇到錯誤,而會得到合理的結果。
一個更微妙的例子是 Flex 版面配置演算法。這個版面配置的概念是將給予 flex render 物件的空間分配給其子元件,因此 flex 的尺寸應該是全部可用空間。在原始設計中,提供無限空間會失敗:這意味著 flex 應該是無限大,這是無用的版面配置設定。相反地,API 調整後,當無限空間分配給 flex render 物件時,render 物件會調整自身大小以符合子元件的期望尺寸,從而減少可能的錯誤情況數量。
這種做法也用於避免建構式允許建立不一致資料。例如,PointerDownEvent 建構式不允許將 PointerEvent 的 down
屬性設為 false(這種情況自相矛盾);相反地,建構式沒有 down 欄位的參數,始終將其設為 true。
一般來說,這種做法是為輸入域中的所有值定義有效的解讀。最簡單的例子是 Color 建構式。它不接受四個整數(分別代表紅、綠、藍、alpha,每個都可能超出範圍),而是預設建構式接受單一整數值,並定義每個位元的含義(例如,最低的八位定義紅色分量),使任何輸入值都是有效的顏色值。
一個更精細的例子是 paintImage() 函式。此函式接受十一個引數,其中一些引數的輸入域很廣,但它們經過精心設計,彼此大多正交,使無效組合非常少。
積極回報錯誤情況
#並非所有錯誤條件都能透過設計來消除。對於剩餘的錯誤,在除錯版本中,Flutter 通常會嘗試在非常早期就捕捉錯誤並立即回報。Asserts 被廣泛使用。建構式引數會被仔細地當場檢查。生命週期會被監控,當檢測到不一致時,立即拋出例外。
在某些情況下,這會走向極端:例如,在執行單元測試時,不論測試在做什麼,每個被版面配置的 RenderBox 子類都會積極地檢查其固有尺寸方法是否履行固有尺寸合約。這有助於捕捉 API 中可能不會被其他方式測試到的錯誤。
當拋出例外時,它們會包含盡可能多的資訊。Flutter 的某些錯誤訊息會主動探查相關的堆疊追蹤,以確定實際 bug 最可能的位置。其他錯誤訊息則會遍歷相關樹以確定不良資料的來源。最常見的錯誤包含詳細說明,有時還包括避免錯誤的範例程式碼,或指向進一步文件的連結。
Reactive 範式
#基於可變樹的 API 存在二元的存取模式:建立樹的初始狀態通常使用與後續更新截然不同的一套操作。Flutter 的渲染層採用此範式,因為這是維護持久樹的有效方式,而持久樹是高效版面配置與繪製的關鍵。然而,這意味著直接與渲染層互動往好說是笨拙,往壞說是容易出 bug。
Flutter 的元件層引入了使用 reactive 範式7的組合機制,以操作底層渲染樹。此 API 透過將樹建立與樹變動步驟合併為單一的樹描述(build)步驟來抽象化樹操作,在此步驟中,每次系統狀態變更後,開發者描述使用者介面的新設定,框架再計算一系列樹變動以反映此新設定。
插值
#由於 Flutter 的框架鼓勵開發者描述符合當前應用程式狀態的介面設定,因此存在一個機制可在這些設定之間進行隱式動畫。
例如,假設在狀態 S1 中,介面由一個圓形組成,而在狀態 S2 中,它由一個正方形組成。沒有動畫機制的話,狀態變化會造成突兀的介面切換。隱式動畫允許圓形在多幀中平滑地過渡為正方形。
每個可以隱式動畫的功能都有一個 stateful 元件,它記錄輸入的當前值,並在輸入值變化時開始動畫序列,在指定的持續時間內從當前值過渡到新值。
這是使用不可變物件的 lerp(線性插值)函式來實現的。每個狀態(在此案例中為圓形和正方形)都表示為一個不可變物件,以適當的設定(顏色、線條寬度等)設定,並知道如何繪製自身。當需要在動畫期間繪製中間步驟時,起始值和結束值會連同代表動畫點的
t 值傳遞到適當的 lerp 函式,其中 0.0 代表 start,1.0 代表 end8,函式返回代表中間階段的第三個不可變物件。
對於圓形到正方形的過渡,lerp 函式會返回一個代表「圓角正方形」的物件,其半徑由 t 值衍生的分數描述、顏色使用顏色的 lerp 函式插值、線條寬度使用雙精度浮點數的
lerp 函式插值。該物件實現與圓形和正方形相同的介面,在被要求時即能繪製自身。
這種技術允許狀態機制、狀態到設定的映射、動畫機制、插值機制,以及關於如何繪製每一幀的具體邏輯彼此完全分離。
這種做法廣泛適用。在 Flutter 中,Color 和 Shape 等基本型別可以插值,但更複雜的型別如 Decoration、TextStyle
或 Theme 也可以。這些型別通常由本身可以插值的元件構成,對更複雜物件的插值往往就像遞迴插值描述複雜物件的所有值一樣簡單。
某些可插值物件由類別階層定義。例如,形狀由 ShapeBorder 介面表示,存在多種形狀,包括 BeveledRectangleBorder、BoxBorder、CircleBorder、RoundedRectangleBorder
與 StadiumBorder。單一的 lerp 函式無法預知所有可能的型別,因此介面定義了 lerpFrom 和 lerpTo
方法,靜態 lerp 方法會委派給這些方法。當被告知要從形狀 A 插值到形狀 B 時,首先詢問 B 是否可以從 A lerpFrom,若不行,則改為詢問 A 是否可以
lerpTo B。(如果兩者都不可能,則函式對 t 值小於 0.5 時返回 A,否則返回 B。)
這讓類別階層可以任意擴展,後續加入的類別能夠在先前已知的值與自身之間進行插值。
在某些情況下,插值本身無法用任何現有類別來描述,此時會定義一個私有類別來描述中間階段。例如,在 CircleBorder 和 RoundedRectangleBorder
之間插值時就是如此。
這個機制還有一個額外的優點:它可以處理從中間階段到新值的插值。例如,在圓形到正方形過渡進行到一半時,形狀可能再次改變,導致動畫需要插值到三角形。只要三角形類別可以從圓角正方形中間類別 lerpFrom,過渡就可以無縫執行。
結論
#Flutter 的口號「一切皆是元件」,圍繞著透過組合元件來建構使用者介面,而這些元件又由更基礎的元件逐層組合而成。這種積極組合的結果是大量的元件,需要精心設計的演算法和資料結構來高效處理。透過一些額外的設計,這些資料結構也讓開發者能夠輕鬆建立在元件變得可見時按需建構元件的無限滾動清單。
腳注:
-
至少在版面配置方面是如此。如有必要,它可能會在繪製時、必要時建構無障礙樹時,以及必要時的點擊測試時被重新訪問。 ↩
-
現實情況當然稍微複雜一些。有些版面配置涉及固有尺寸或基線測量,這確實需要對相關子樹進行額外的遍歷(積極的快取用於緩解最壞情況下潛在的二次方效能)。然而,這些情況出乎意料地罕見。特別是,固有尺寸在縮包(shrink-wrapping)的常見情況下並不需要。 ↩
-
嚴格來說,子物件的位置不是其 RenderBox geometry 的一部分,因此實際上不需要在版面配置期間計算。許多 render 物件隱式地將其單一子物件定位在相對於自身原點的 0,0 處,完全不需要計算或儲存。有些 render 物件會盡可能延遲計算子物件的位置(例如在繪製階段),以便在後續不需要繪製時完全避免計算。 ↩
-
此規則有一個例外。如 按需建構元件 節所述,某些元件可能因版面配置 constraints 的變化而被重建。如果一個元件在同一幀中因不相關原因將自身標記為 dirty,並且也受到版面配置 constraints 變化的影響,它將被更新兩次。這種冗餘的 build 僅限於元件本身,不影響其後代。 ↩
Key 是一個可選地與元件關聯的不透明物件,其相等運算子用於影響對帳演算法。 ↩
-
為了無障礙性,以及讓應用程式在元件建構與顯示在螢幕上之間多幾毫秒的時間,視口會在可見元件前後幾百像素建立(但不繪製)元件。 ↩
這種做法最初由 Facebook 的 React 函式庫推廣普及。 ↩
-
實際上,t 值允許超出 0.0-1.0 的範圍,某些曲線確實會這樣做。例如,「彈性」曲線會短暫超調以呈現彈跳效果。插值邏輯通常可以在適當情況下外推超出起點或終點。對於某些型別,例如在插值顏色時,t 值實際上會被截斷到 0.0-1.0 範圍內。 ↩
Unless stated otherwise, the documentation on this site reflects Flutter 3.44.0. Page last updated on 2026-06-14. View source or report an issue.