1. 程式人生 > >【翻譯】flutter的設計哲學( inside flutter)

【翻譯】flutter的設計哲學( inside flutter)

英文連結來自於flutter官網

對應的版本為11月11日版本 連線為github

後續如有改動,請以最新的英文版本為準,有翻譯不準確的地方請參照英文版本自行理解


概述


本文件描述了Flutter工具包的內部工作原理,使Flutter的API成為可能。因為Flutter小部件是使用積極的可組合性(aggressive composition)構建的,所以使用Flutter構建的使用者介面具有大量小部件。

為了支援這種工作量,Flutter使用次線性演算法來佈局和構建小部件,這些資料結構使樹形手術變得高效,並且具有許多常量因子優化。

通過一些額外的細節,這種設計還使開發人員可以使用回撥來輕鬆建立無限滾動列表,這些回撥可以構建使用者可見的小部件。

積極的可組合性(Aggressive composability)


Flutter最獨特的一個方面是其積極的可組合性。

小部件是通過組合其他小部件構建的,這些小部件本身是由逐步更基本的小部件構建的。例如,Padding是一個小部件而不是其他小部件的屬性。因此,使用Flutter構建的使用者介面由許多小部件組成。

小部件構建遞迴在RenderObjectWidgets中觸底,這些小部件在底層渲染樹中建立節點。渲染樹是一種資料結構,用於儲存使用者介面的幾何圖形,該幾何圖形在佈局期間計算並在繪製和命中測試期間使用。大多數Flutter開發人員不直接建立物件,而是使用小部件操縱渲染樹。

為了在小部件層支援積極的可組合性,Flutter在小部件和渲染樹層使用了許多有效的演算法和優化,這些將在以下小節中介紹。

次線性佈局

使用大量小部件和渲染物件,良好效能的關鍵是高效的演算法。最重要的是佈局的效能,佈局是確定渲染物件的幾何(例如,大小和位置)的演算法。其他一些工具包使用O(N²)或更差的佈局演算法(例如,某些約束域中的定點迭代)。 Flutter的目標是初始佈局的線性效能,以及隨後更新現有佈局的常見情況下的次線性佈局效能。通常,佈局所花費的時間量應該比渲染物件的數量更慢。

Flutter每幀執行一個佈局,佈局演算法一次完成。約束通過父物件向下傳遞,父物件在每個子物件上呼叫佈局方法。子項遞迴地執行自己的佈局,然後通過返回佈局方法將幾何返回到樹中。重要的是,一旦渲染物件從其佈局方法返回,該渲染物件將不再被訪問,直到下一幀的佈局。這種方法將可能單獨的度量和佈局傳遞組合成單個傳遞,因此,每個渲染物件在佈局期間最多訪問兩次:一次在樹下,一次在樹上。

Flutter有這個通用協議的幾個專業。最常見的專業是RenderBox,它以二維笛卡爾座標運算。在框佈局中,約束是最小和最大寬度以及最小和最大高度。在佈局期間,子項通過選擇這些邊界內的大小來確定其幾何。孩子從佈局返回後,父母決定孩子在父母座標系中的位置。請注意,孩子的佈局不能取決於孩子的位置,因為孩子的位置直到孩子從佈局返回後才確定。因此,父母可以自由地重新定位孩子,而無需重新計算孩子的佈局。

更一般地說,在佈局期間,從父節點傳遞到子節點的唯一資訊是約束,並且從子節點流向父節點的唯一資訊是幾何體。這些不變數可以減少佈局期間所需的工作量:

  • 如果孩子沒有將自己的佈局標記為髒,則孩子可以立即從佈局返回,切斷步行,只要父母給孩子的約束與孩子在前一個佈局中收到的約束相同。

  • 每當父級呼叫子級的佈局方法時,父級指示它是否使用從子級返回的大小資訊。如果經常發生父級不使用大小資訊,那麼如果子級選擇新大小,則父級不需要重新計算其佈局,因為父級保證新大小將符合現有約束。

  • 嚴格約束是指只能通過一個有效幾何體來滿足的約束。例如,如果最小和最大寬度彼此相等並且最小和最大高度彼此相等,則滿足這些約束的唯一尺寸是具有該寬度和高度的尺寸。如果父級提供嚴格約束,則父級無需在子級重新計算其佈局時重新計算其佈局,即使父級在其佈局中使用子級的大小,因為子級無法在沒有父級的新約束的情況下更改大小。

  • 渲染物件可以宣告它僅使用父級提供的約束來確定其幾何。這樣的宣告通知框架該子渲染物件的父級在子級重新計算其佈局時不需要重新計算其佈局,即使約束不緊,即使父級的佈局取決於子級的大小,因為子級無法更改大小沒有來自其父級的新約束。

作為這些優化的結果,當渲染物件樹包含髒節點時,在佈局期間僅訪問那些節點以及它們周圍的子樹的有限部分。

次線性小部件構建

與佈局演算法類似,Flutter的小部件構建演算法是次線性的。構建之後,小部件由元素樹儲存,元素樹保留使用者介面的邏輯結構。元素樹是必要的,因為小部件本身是不可變的,這意味著(除其他外),它們不能記住它們與其他小部件的父或子關係。元素樹還包含與有狀態視窗小部件關聯的狀態物件。

響應於使用者輸入(或其他刺激),元素可能變髒,例如,如果開發人員在關聯的狀態物件上呼叫setState()。框架保留一個髒元素列表,並在構建階段直接跳轉到它們,跳過乾淨的元素。在構建階段,資訊在元素樹中單向流動,這意味著在構建階段期間每個元素最多訪問一次。清潔後,元素不會再次變髒,因為通過感應,它的所有祖先元素也都是乾淨的。

因為視窗小部件是不可變的,所以如果元素沒有將自身標記為髒,則元素可以立即從構建返回,如果父級使用相同的視窗小部件重建元素,則會切斷步行。此外,元素只需要比較兩個視窗小部件引用的物件標識,以確定新視窗小部件與舊視窗小部件相同。開發人員利用此優化來實現重投影模式,其中視窗小部件包括在其構建中儲存為成員變數的預構建子視窗小部件。

在構建期間,Flutter還避免使用InheritedWidgets遍歷父鏈。如果視窗小部件通常走他們的父鏈,例如確定當前的主題顏色,則構建階段將在樹的深度變為O(N 2),由於積極的組合,這可能非常大。為了避免這些父行為,框架通過在每個元素上維護一個InheritedWidgets的雜湊表來向下推送元素樹中的資訊。通常,許多元素將引用相同的雜湊表,該雜湊表僅在引入新的InheritedWidget的元素上更改。

線性對比

與流行的看法相反,Flutter不使用樹差異演算法。相反,框架通過使用O(N)演算法獨立地檢查每個元素的子列表來決定是否重用元素。子列表協調演算法針對以下情況進行了優化:

  • 舊子列表為空。
  • 兩個列表是相同的。
  • 在列表中的一個位置插入或刪除一個或多個小部件。
  • 如果每個列表包含具有相同鍵的視窗小部件,則匹配兩個視窗小部件。

一般方法是通過比較每個小部件的執行時型別和鍵來匹配兩個子列表的開頭和結尾,可能在包含所有不匹配子節點的每個列表的中間找到非空範圍。然後,框架將舊子列表中的子項放入基於其鍵的雜湊表中。接下來,框架遍歷新子列表中的範圍,並按鍵查詢雜湊表以進行匹配。無法對比的孩子被丟棄並從頭開始重建,而匹配的孩子則用他們的新小部件重建。

樹手術(Tree surgery)

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

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

重新構造的子樹中的渲染物件能夠保留其佈局資訊,因為佈局約束是渲染樹中從父級傳遞到子級的唯一資訊。新父級被標記為髒,因為其子列表已更改,但如果新父級傳遞子級具有子級從其舊父級接收的相同佈局限制,則子級可以立即從佈局返回,從而切斷遍歷。

開發人員廣泛使用全域性鍵和非本地樹突變來實現英雄過渡和導航等效果。

恆定因子優化(Constant-factor optimizations)

除了這些演算法優化之外,實現積極的可組合性還依賴於幾個重要的恆定因子優化。這些優化在上面討論的主要演算法的葉子中是最重要的。

  • 子佈局模型不可知論者。與大多數使用子列表的工具包不同,Flutter的渲染樹不會提交給特定的子模型。例如,RenderBox類有一個抽象的visitChildren()方法,而不是具體的firstChild和nextSibling介面。許多子類僅支援單個子項,直接作為成員變數而不是子項列表。例如,RenderPadding僅支援單個子節點,因此,佈局方法更簡單,執行時間更短。
  • 視覺渲染樹,邏輯小部件樹。在Flutter中,渲染樹在與裝置無關的視覺座標系中操作,這意味著即使當前讀取方向是從右到左,x座標中的較小值也始終向左。小部件樹通常在邏輯座標中操作,意味著具有開始和結束值,其視覺解釋取決於閱讀方向。從邏輯座標到可視座標的轉換是在小部件樹和渲染樹之間的切換中完成的。這種方法更有效,因為渲染樹中的佈局和繪製計算比視窗小部件到渲染樹的切換更頻繁,並且可以避免重複的座標轉換。
  • 由專門的渲染物件處理的文字。絕大多數渲染物件都不瞭解文字的複雜性。相反,文字由專門的渲染物件RenderParagraph處理,RenderParagraph是渲染樹中的葉子。開發人員不是使用文字感知渲染物件進行子類化,而是使用合成將文字合併到使用者介面中。這種模式意味著RenderParagraph可以避免重新計算其文字佈局,只要其父級提供相同的佈局約束,這是常見的,即使在樹手術期間也是如此。
  • 可觀察的物件。顫動使用模型觀察和反應範例。顯然,反應正規化占主導地位,但Flutter對一些葉子資料結構使用可觀察的模型物件。例如,動畫在值發生變化時通知觀察者列表。顫動將這些可觀察物件從小部件樹移交給渲染樹,渲染樹直接觀察它們並且在它們改變時僅使管道的適當階段無效。例如,對動畫的更改可能僅觸發繪製階段,而不是構建和繪製階段。

這些優化結合起來並總結了積極構圖所產生的大樹,對效能產生了重大影響。

無限滾動

無限的滾動列表對於工具包來說是非常困難的。 Flutter支援基於構建器模式的簡單介面的無限滾動列表,其中ListView使用回撥按需構建視窗小部件,因為它們在滾動期間對使用者可見。支援此功能需要視口感知佈局和按需構建視窗小部件。

視口感知佈局

像Flutter中的大多數東西一樣,可滾動的小部件是使用合成構建的。可滾動視窗小部件的外部是一個視口,它是一個“內部較大”的框,這意味著它的子視窗可以超出視口的邊界,並可以滾動到檢視中。但是,視口不是具有RenderBox子元素,而是具有RenderSliv​​er子元素,稱為Slivers,具有視口感知佈局協議。

Slivers佈局協議與框佈局協議的結構相匹配,因為父級將約束傳遞給子級並返回接收幾何。但是,約束和幾何資料在兩個協議之間不同​​。在Slivers協議中,向孩子們提供有關視口的資訊,包括剩餘的可見空間量。它們返回的幾何資料可實現各種滾動連結效果,包括可摺疊標題和視差。

不同的條紋以不同的方式填充視口中可用的空間。例如,產生兒童線性列表的Slivers按順序排列每個孩子,直到Slivers用完兒童或用完空間。類似地,產生二維兒童網格的Slivers僅填充其可見的網格部分。因為他們知道可以看到多少空間,所以即使他們有可能產生無限數量的兒童,也可以產生有限數量的兒童。

可以組合Slivers來建立定製的可滾動佈局和效果。例如,單個視口可以具有可摺疊標題,後跟線性列表,然後是網格。所有三個Slivers都將通過Slivers佈局協議進行協作,以僅生成通過視口實際可見的子項,無論這些子項是否屬於標題,列表或網格。

按需構建小部件

如果Flutter有一個嚴格的構建 - 然後 - 佈局 - 然後 - 繪製管道,前面的內容將不足以實現無限滾動列表,因為通過視口可以看到多少空間的資訊僅在佈局階段可用。如果沒有額外的機器,佈局階段就太晚了,無法構建填充空間所需的小部件。 Flutter通過交錯管道的構建和佈局階段來解決這個問題。在佈局階段的任何時候,只要這些小部件是當前執行佈局的渲染物件的後代,框架就可以開始按需構建新的小部件。

只有在構建和佈局演算法中對資訊傳播進行嚴格控制,才能實現交錯構建和佈局。具體而言,在構建階段,資訊只能在樹下傳播。當渲染物件正在執行佈局時,佈局遍歷沒有訪問該渲染物件下面的子樹,這意味著通過在該子樹中構建而生成的寫入不能使到目前為止已進入佈局計算的任何資訊無效。類似地,一旦佈局從渲染物件返回,在此佈局期間將永遠不再訪問該渲染物件,這意味著後續佈局計算生成的任何寫入都不會使用於構建渲染物件的子樹的資訊無效。

此外,線性協調和樹形外觀對於在滾動期間有效更新元素以及在元素在視口邊緣滾動進出檢視時修改渲染樹至關重要。

API人體工程學(API Ergonomics)

快速只有在框架能夠有效使用時才有意義。為了引導Flutter的API設計更具可用性,Flutter已經在開發人員的廣泛使用者體驗研究中反覆測試過。這些研究有時會確認已有的設計決策,有時可以幫助指導功能的優先順序,有時會改變API設計的方向。例如,Flutter的API記錄很多;使用者體驗研究證實了此類文件的價值,但也強調了專門針對示例程式碼和說明性圖表的需求。

本節討論Flutter API設計中為幫助可用性而做出的一些決策。

專門用於匹配開發人員思維模式的API

Flutter的Widget,Element和RenderObject樹中節點的基類未定義子模型。這允許每個節點專用於適用於該節點的子模型。

大多數Widget物件都有一個子Widget,因此只顯示一個子引數。一些小部件支援任意數量的子節點,並公開帶有列表的子引數。有些小部件根本沒有任何子節點,並且沒有記憶體,也沒有任何引數。同樣,RenderObjects公開特定於其子模型的API。 RenderImage是一個葉子節點,沒有子節點的概念。 RenderPadding只佔用一個子節點,因此它具有儲存單指標的單個指標。 RenderFlex佔用任意數量的子節點並將其作為連結串列進行管理。

在極少數情況下,使用更復雜的子模型。 RenderTable渲染物件的建構函式接受一個子陣列陣列,該類公開控制行數和列數的getter和setter,並且有一些特定的方法用x,y座標替換單個子節點,以新增一行,以提供一個新的子陣列陣列,並用一個數組和一個列數替換整個子列表。在實現中,物件不像大多數渲染物件那樣使用連結列表,而是使用可索引陣列。

Chip小部件和InputDecoration物件具有與相關控制元件上存在的插槽匹配的欄位。如果一個通用的子模型將強制語義分層在子列表之上,例如,將第一個子項定義為字首值,將第二個子項定義為字尾,則專用子模型允許用於替代使用的專用命名屬性。

這種靈活性允許這些樹中的每個節點以其最常用的角色進行操作。很少想要在表格中插入一個單元格,導致所有其他單元格環繞;同樣,很少想要通過索引而不是通過引用從flex行中刪除子項。

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

專門用於滿足開發人員期望的API的整體方法不僅適用於兒童模型。

一些相當瑣碎的小部件專門存在,以便開發人員在尋找問題的解決方案時會找到它們。一旦知道如何使用Expanded小部件和零大小的SizedBox子級,就可以輕鬆地為行或列新增空格,但發現該模式是不必要的,因為搜尋空間會揭示Spacer小部件,它直接使用Expanded和SizedBox達到效果。

類似地,通過根本不在構建中包括視窗小部件子樹,可以容易地隱藏視窗小部件子樹。但是,開發人員通常希望有一個小部件來執行此操作,因此存在可見性小部件以將此模式包裝在一個簡單的可重用小部件中。

明確的論點(Explicit arguments)

UI框架往往具有許多屬性,因此開發人員很少能夠記住每個類的每個建構函式引數的語義含義。由於Flutter使用反應範例,因此Flutter中的構建方法通常會對建構函式進行多次呼叫。通過利用Dart對命名引數的支援,Flutter的API能夠使這些構建方法保持清晰易懂。

此模式擴充套件到具有多個引數的任何方法,特別是擴充套件到任何布林引數,以便方法呼叫中隔離的true或false文字始終是自我記錄的。此外,為避免通常由API中的雙重否定引起的混淆,布林引數和屬性始終以正形式命名(例如,enabled:true而不是disabled:false)。

攤鋪陷阱(Paving over pitfalls)

在Flutter框架中的許多地方使用的技術是定義API,使得不存在錯誤條件。這樣可以避免考慮整個錯誤類別。

例如,插值函式允許插值的一端或兩端為空,而不是將其定義為錯誤情況:兩個空值之間的插值始終為空,並且從空值或空值插值等效於對給定型別插值為零模擬。這意味著不小心將null傳遞給插值函式的開發人員不會遇到錯誤情況,而是會獲得合理的結果。

一個更微妙的例子是Flex佈局演算法。這種佈局的概念是賦予flex渲染物件的空間在其子節點之間劃分,因此flex的大小應該是整個可用空間。在原始設計中,提供無限空間會失敗:這意味著flex應該是無限大小的,無用的佈局配置。相反,調整了API,以便在為flex渲染物件分配無限空間時,渲染物件會調整其大小以適應子節點的所需大小,從而減少可能的錯誤情況數。

該方法還用於避免具有允許建立不一致資料的建構函式。例如,PointerDownEvent建構函式不允許將PointerEvent的down屬性設定為false(這種情況會自相矛盾);相反,建構函式沒有down欄位的引數,並始終將其設定為true。

通常,該方法是為輸入域中的所有值定義有效解釋。最簡單的例子是Color建構函式。而不是取四個整數,一個用於紅色,一個用於綠色,一個用於藍色,一個用於alpha,每個都可能超出範圍,預設建構函式採用單個整數值,並定義每個位的含義(對於例如,底部的八位定義紅色分量),因此任何輸入值都是有效的顏色值。

一個更精細的例子是paintImage()函式。此函式需要11個引數,其中一些具有相當寬的輸入域,但它們經過精心設計,大部分彼此正交,因此很少有無效組合。

積極報告錯誤案例

聆聽翻譯 並非所有錯誤條件都可以設計出來。對於那些剩下的,在除錯版本中,Flutter通常會嘗試很早地捕獲錯誤並立即報告它們。斷言被廣泛使用。建構函式引數詳細檢查完整性。監視生命週期,當檢測到不一致時,它們會立即引發異常。

在某些情況下,這是極端的:例如,在執行單元測試時,無論測試正在做什麼,每個積極佈局的RenderBox子類都會檢查其內在的大小調整方法是否滿足內部大小調整合同。這有助於捕獲可能無法執行的API中的錯誤。

丟擲異常時,它們包含儘可能多的資訊。 Flutter的一些錯誤訊息會主動探測相關的堆疊跟蹤,以確定實際錯誤的最可能位置。其他人走相關樹來確定不良資料的來源。最常見的錯誤包括詳細說明,包括在某些情況下用於避免錯誤的示例程式碼或指向其他文件的連結。

響應式正規化(Reactive paradigm)

可變的基於樹的API受二元訪問模式的影響:建立樹的原始狀態通常使用與後續更新完全不同的操作集。 Flutter的渲染層使用這種範例,因為它是維護持久樹的有效方法,這是高效佈局和繪畫的關鍵。然而,這意味著與渲染層的直接互動充其量是尷尬的,並且在最壞的情況下容易出錯。

Flutter的小部件層引入了一個使用反應正規化來操作底層渲染樹的組合機制。此API通過將樹建立和樹變非同步驟組合到單個樹描述(構建)步驟中抽象出樹操作,其中,在每次更改系統狀態之後,開發人員描述使用者介面的新配置。 framework計算反映這種新配置所必需的一系列樹突變。

插值(Interpolation )

由於Flutter的框架鼓勵開發人員描述與當前應用程式狀態匹配的介面配置,因此存在一種在這些配置之間隱式動畫的機制。

例如,假設在狀態S1中介面由一個圓組成,但在狀態S2中它由一個正方形組成。如果沒有動畫機制,狀態更改將會有一個不和諧的介面更改。隱式動畫允許圓在幾幀上平滑平方。

每個可以隱式動畫的特徵都有一個有狀態小部件,它儲存輸入當前值的記錄,並在輸入值改變時開始動畫序列,在指定的持續時間內從當前值轉換為新值。

這是使用lerp(線性插值)函式使用不可變物件實現的。每個狀態(在這種情況下為圓形和方形)表示為不可變物件,配置有適當的設定(顏色,筆觸寬度等)並知道如何繪製自身。當在動畫期間繪製中間步驟時,將開始和結束值傳遞給適當的lerp函式以及表示動畫點的值,其中0.0表示開始,1.0表示結束,函式返回表示中間階段的第三個不可變物件。

對於圓到邊的過渡,lerp函式將返回一個表示“圓角正方形”的物件,其半徑描述為從t值匯出的分數,使用顏色的lerp函式插值的顏色和筆劃使用lerp函式對雙精度進行插值。該物件實現與圓形和正方形相同的介面,然後可以在請求時繪製自己。

這種技術允許狀態機制,狀態到配置的對映,動畫機制,插值機制,以及與如何繪製每個幀完全相互分離有關的特定邏輯。

這種方法廣泛適用。在Flutter中,可以插入基本型別(如顏色和形狀),但也可以使用更精細的型別,例如裝飾,TextStyle或主題。這些通常由可以自己插值的元件構成,並且插入更復雜的物件通常就像遞迴插值描述複雜物件的所有值一樣簡單。

某些可插入物件由類層次結構定義。例如,形狀由ShapeBorder介面表示,並且存在各種形狀,包括BeveledRectangleBorder,BoxBorder,CircleBorder,RoundedRectangleBorder和StadiumBorder。單個lerp函式不能具有所有可能型別的先驗知識,因此介面定義了lerpFrom和lerpTo方法,靜態lerp方法遵循這些方法。當被告知從形狀A插入到形狀B時,首先詢問它是否可以從A變形,然後,如果它不能,則向A詢問它是否可以變為B.(如果兩者都不可能,則該函式返回A從t小於0.5的值,否則返回B.)

這允許任意擴充套件類層次結構,後面的新增能夠在先前已知的值和它們自身之間進行插值。

在某些情況下,插值本身不能由任何可用類描述,並且定義私有類來描述中間階段。例如,在CircleBorder和RoundedRectangleBorder之間進行插值時就是這種情況。

這種機制還有一個額外的優點:它可以處理從中間階段到新值的插值。例如,在圓形到方形過渡的中途,形狀可以再次改變,導致動畫需要插入到三角形。只要三角形類可以從圓角方形中間類中獲得,就可以無縫地執行轉換。

結論(Conclusion)

Flutter的口號“一切都是小部件”,圍繞著構建使用者介面,通過組合小部件來構建,小部件又由逐步更基本的小部件組成。這種積極組合的結果是大量的小部件需要精心設計的演算法和資料結構才能有效地處理。通過一些額外的設計,這些資料結構還使開發人員可以輕鬆建立無限滾動列表,以便在可見時按需構建視窗小部件。