Jetpack Compose 階段

如同多數其他 UI 工具包,Compose 會透過數個影格轉譯影格 不同的階段。我們來看 Android View 系統 包括測量、版面配置和繪圖Compose 是非常類似的 開始時稱為「組合」的重要額外階段

「組合」在我們的 Compose 文件中皆有描述,包括 Compose狀態與 Jetpack Compose

呈現畫面的三個階段

Compose 有三個主要階段:

  1. 組合:決定要顯示「哪些」UI。Compose 會執行可組合函式 建立 UI 說明。
  2. 版面配置安排 UI 的「位置」。這個階段包含兩個步驟: 衡量與刊登位置版面配置元素會測量並放置本身 2D 座標中的任何子項元素,也就是版面配置樹狀結構中每個節點的子元素。
  3. 繪圖設定「算繪」方式。UI 元素會繪製在 Canvas 中,這通常是 裝置螢幕畫面。
圖片中的三個階段,Compose 將資料轉換為 UI (依序、資料、組合、版面配置、繪圖和 UI)。
圖 1. Compose 將資料轉換為 UI 的三個階段。

這些階段的順序大致相同,資料可在一個階段流入 從組合、版面配置到繪圖的方向,藉此產生影格 (已知 做為「單向資料流」)。 BoxWithConstraints 和 以 LazyColumnLazyRow 來說 例外狀況,因為其子項的組合取決於父項的版面配置 事實上,Gartner 的資料顯示 只有一半的企業機器學習專案通過前測階段

您可以放心假設每個影格幾乎都會發生這三個階段 但為了提高效能,Compose 避免重複執行會 在所有階段中,來自相同輸入的相同結果來計算相同結果。撰寫 skips 來執行可組合函式 函式 (如果可以重複使用先前結果),而且 Compose UI 不會重新版面配置或 在非必要的情況下重新繪製整個樹狀結構Compose 只會執行 更新 UI 所需的最低工作量您可以採用這種最佳化方式 因為 Compose 會在不同階段內追蹤讀取的狀態。

瞭解各階段

本節說明如何針對可組合項執行三個 Compose 階段 一起來看看吧

樂曲

在組合階段,Compose 執行階段會執行可組合函式, 輸出代表 UI 的樹狀結構。這個 UI 樹狀結構包含 包含下一階段所需的所有資訊的版面配置節點 範例:

圖 2. 代表在組合中建立的 UI 的樹狀結構 事實上,Gartner 的資料顯示 只有一半的企業機器學習專案通過前測階段

程式碼和 UI 樹狀結構的子部分如下所示:

包含五個可組合項及其產生的 UI 樹狀結構的程式碼片段,而子節點是從父項節點分支。
圖 3. UI 樹狀結構的子區段和對應的程式碼。

在這些範例中,程式碼中的每個可組合函式都會對應至單一版面配置 建立節點在更複雜的範例中,可組合函式可包含邏輯和 產生不同狀態的樹狀結構

版面配置

在版面配置階段,Compose 會使用組合階段產生的 UI 樹狀結構 做為輸入內容版面配置節點集合含有 決定每個節點的大小和在 2D 空間中的位置

圖 4. 在版面配置階段,於 UI 樹狀結構中測量每個版面配置節點和位置。

在版面配置階段,系統會透過下列三個步驟週遊樹狀結構 演算法:

  1. 測量子項:節點會測量子項 (如有)。
  2. 決定大小:依據這些測量結果,節點會自行決定大小 大小
  3. 放置子項:每個子節點都放置於節點本身的 位置。

在這個階段結束時,每個版面配置節點都有以下功能:

  • 指派的「寬度」和「高度」
  • 應繪製 x、y 座標

從上一節呼叫 UI 樹狀結構:

含有五個可組合項及其產生的 UI 樹狀結構的程式碼片段,而子節點是從父項節點分支

在這個樹狀結構中,演算法的運作方式如下:

  1. Row 會測量其子項 ImageColumn
  2. Image 是測量值。目前沒有任何子項,因此自行定義 並將大小回報為 Row
  3. 接著會測量 Column。測量本身子項 (兩個Text) 可組合函式)。
  4. 系統會測量第一個 Text。目前沒有任何子項,因此可決定 並將大小回報為 Column
    1. 系統會測量第二個 Text。目前沒有任何子項,因此可決定 並回報給 Column
  5. Column 會使用子項測量結果決定本身的大小。該元件會使用 子項寬度及其子項高度的總和。
  6. Column 會將子項相對置於下方,並放在下方 彼此垂直移動
  7. Row 會使用子項測量結果決定本身的大小。該元件會使用 子項高度上限和子項寬度總和。然後計算 所有子項。

請注意,每個節點僅造訪一次。Compose 執行階段只需要一個 經 UI 樹狀結構,以測量和放置所有節點, 才需進行當樹狀圖中的節點數量增加時,表示所花費的時間 以線性方式持續成長相反地,如果每個節點 多次造訪,週遊時間會急劇增加。

繪圖

在繪圖階段,樹狀結構會由上而下再次週遊, 節點會依序在螢幕上顯示

圖 5. 繪圖階段會繪製螢幕上的像素。

根據上例,樹狀內容是依照下列方式繪製:

  1. Row 會繪製其可能包含的任何內容,例如背景顏色。
  2. Image 會自行繪製。
  3. Column 會自行繪製。
  4. 第一個和第二個 Text 會分別繪製。

圖 6. UI 樹狀結構及其繪製的表示法。

狀態讀取

當您在測試期間讀取快照狀態的值時 Compose 會自動追蹤 則讀取該值。這項追蹤功能可讓 Compose 在 狀態值變更,也是 Compose 狀態觀測能力的基礎。

狀態通常會使用 mutableStateOf() 建立,然後透過單一狀態存取 方法有兩種:直接存取 value 屬性,或者 您可以使用 Kotlin 屬性委派功能來確認專案狀態相關詳情請參閱美國各州 可組合函式用途 「狀態讀取」是指上述任一項同等存取權 方法。

// State read without property delegate.
val paddingState: MutableState<Dp> = remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(paddingState.value)
)

// State read with property delegate.
var padding: Dp by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.padding(padding)
)

定義屬性 委派、 「getter」和「setter」函式可用於存取及更新 State 的 value。只有在您參照這些函式時,系統才會叫用 getter 和 setter 函式 將屬性設為值而非建立時,這就是原因 以上規則相等。

每個程式碼區塊可在讀取狀態變更時重新執行 重新啟動範圍Compose 會追蹤狀態值變更並重新啟動 分為不同的階段

階段性狀態讀取

如上所述,Compose 有三個主要階段,而 Compose 測試群組 每個容器中會讀取的狀態為何這樣一來,Compose 就只會傳送 必須為每個受影響的元素執行工作 第一種是使用無代碼解決方案 AutoML 透過使用者介面建立機器學習模型

現在讓我們深入瞭解各個階段,說明讀取狀態值時會發生的情況 在其中建立目錄

階段 1:組合

@Composable 函式或 lambda 區塊中讀取狀態會影響組合 甚至可能進行後續階段當狀態值變更時, 重組工具排程重新執行所有讀取該函式的可組合函式 狀態值。請注意,執行階段可能會決定略過部分或所有 可組合函式。請參閱如果輸入內容確實為空值,則請略過。

Compose UI 會依據組合結果執行版面配置和繪圖 階段如果內容維持不變,尺寸和尺寸一樣,系統可能會略過這些階段 版面配置也不會改變

var padding by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    // The `padding` state is read in the composition phase
    // when the modifier is constructed.
    // Changes in `padding` will invoke recomposition.
    modifier = Modifier.padding(padding)
)

階段 2:版面配置

版面配置階段包含兩個步驟:「測量」和「放置」。 測量步驟會執行傳遞至 Layout 可組合函式的測量 lambda, LayoutModifier 介面的 MeasureScope.measure 方法,依此類推。 放置步驟會執行 layout 函式的位置區塊 (lambda) Modifier.offset { … } 區塊,依此類推

每個步驟的狀態讀取作業都會影響版面配置, 繪製階段當狀態值變更時,Compose UI 會排定版面配置 事實上,Gartner 的資料顯示 只有一半的企業機器學習專案通過前測階段如果尺寸或位置發生變更,也會執行繪圖階段。

更精確地說,評估步驟和刊登位置步驟 重啟範圍,這表示放置步驟中的狀態讀取作業不會重新叫用 不過,這兩個步驟 因此,放置步驟中的狀態讀取作業可能會影響其他重新啟動作業 屬於評估步驟的範圍

var offsetX by remember { mutableStateOf(8.dp) }
Text(
    text = "Hello",
    modifier = Modifier.offset {
        // The `offsetX` state is read in the placement step
        // of the layout phase when the offset is calculated.
        // Changes in `offsetX` restart the layout.
        IntOffset(offsetX.roundToPx(), 0)
    }
)

階段 3:繪圖

繪圖程式碼執行期間的狀態讀取作業會影響繪圖階段。常見範例 包括 Canvas()Modifier.drawBehindModifier.drawWithContent。時間 當狀態值變更時,Compose UI 只會執行繪圖階段。

var color by remember { mutableStateOf(Color.Red) }
Canvas(modifier = modifier) {
    // The `color` state is read in the drawing phase
    // when the canvas is rendered.
    // Changes in `color` restart the drawing.
    drawRect(color)
}

最佳化狀態讀取作業

當 Compose 執行本地化的狀態讀取追蹤時,我們可以盡量減少 讀取適當階段中的每個狀態並執行相關工作。

一起來看看這個範例。此處的 Image() 使用偏移值 修飾符來偏移其最終版面配置位置,進而產生視差效果, 使用者捲動畫面

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        // Non-optimal implementation!
        Modifier.offset(
            with(LocalDensity.current) {
                // State read of firstVisibleItemScrollOffset in composition
                (listState.firstVisibleItemScrollOffset / 2).toDp()
            }
        )
    )

    LazyColumn(state = listState) {
        // ...
    }
}

這段程式碼可以運作,但會導致效能不佳。如上所述 讀取 firstVisibleItemScrollOffset 狀態的值,並傳遞至 這個 Modifier.offset(offset: Dp) 函式。當使用者捲動畫面時,firstVisibleItemScrollOffset 值會增加 變更。如我們所知,Compose 會追蹤所有讀取的狀態,以便能夠重新啟動 讀取程式碼,在本例中為 Box

這是在組合階段內讀取的狀態範例。這是 但這不一定是壞事,事實上這是重組的基礎 允許資料變更輸出新的 UI。

本例並非最理想的做法,因為每次捲動事件都會產生 並重新評估整個可組合項的內容 版面配置我們會在每次捲動時觸發撰寫階段 即使顯示的內容沒有改變,只是改變內容的顯示位置。 不過,我們可以對狀態讀取作業進行最佳化,從而只重新觸發版面配置階段。

系統提供另一個版本的偏移修飾符: Modifier.offset(offset: Density.() -> IntOffset)

這個版本採用 lambda 參數,結果會由以下參數傳回: lambda 區塊讓我們更新程式碼來使用這個版本:

Box {
    val listState = rememberLazyListState()

    Image(
        // ...
        Modifier.offset {
            // State read of firstVisibleItemScrollOffset in Layout
            IntOffset(x = 0, y = listState.firstVisibleItemScrollOffset / 2)
        }
    )

    LazyColumn(state = listState) {
        // ...
    }
}

那麼為什麼這種做法效能較佳?我們提供給修飾符的 lambda 區塊是 在「版面配置」階段 (特別是版面配置階段的 刊登位置步驟),這表示 firstVisibleItemScrollOffset 狀態表示 可在組合期間讀取資料的時間較長由於 Compose 會追蹤狀態讀取時間 這表示如果 firstVisibleItemScrollOffset 值有所變更, Compose 只需要重新啟動版面配置和繪圖階段。

這個範例仰賴不同的偏移修飾符,才能最佳化 但這個概念適合一般情況:請嘗試將狀態讀取作業本地化 也能讓 Compose 執行 這些研究有助於我們找出 能引導後續作業的標準

當然,在組合中讀取狀態經常是絕對必要的 事實上,Gartner 的資料顯示 只有一半的企業機器學習專案通過前測階段即使如此,在某些情況下 透過篩選狀態變更來重組如要進一步瞭解相關資訊 請參閱 derivedStateOf:將一或多個狀態物件轉換成其他 狀態

重組迴圈 (循環階段依附元件)

我們先前提過,Compose 階段一律會以相同的方式叫用 而且在同一影格中無法倒轉, 但這並非禁止應用程式進入組合迴圈 「不同」影格中的對應表現請參考以下例子:

Box {
    var imageHeightPx by remember { mutableStateOf(0) }

    Image(
        painter = painterResource(R.drawable.rectangle),
        contentDescription = "I'm above the text",
        modifier = Modifier
            .fillMaxWidth()
            .onSizeChanged { size ->
                // Don't do this
                imageHeightPx = size.height
            }
    )

    Text(
        text = "I'm below the image",
        modifier = Modifier.padding(
            top = with(LocalDensity.current) { imageHeightPx.toDp() }
        )
    )
}

我們在此 (糟糕) 實作了一個垂直欄,並在上方放置圖片 然後是下方的文字使用「Modifier.onSizeChanged()」掌握 已解析的圖片大小,然後對文字使用 Modifier.padding() 移動 Pod從 Px 返回 Dp 的不自然轉換已 表示程式碼有問題。

這個範例的問題是無法得出「最終」版面配置 提供單一畫面程式碼需要多個影格才能運作, 並導致 UI 在螢幕上跳動。

讓我們逐步觀察每個畫面,看看發生了什麼情況:

在第一個畫面的組合階段,imageHeightPx 的值為 0。 文字則由 Modifier.padding(top = 0) 提供。接著是版面配置 階段,系統會呼叫 onSizeChanged 修飾符的回呼。 這時 imageHeightPx 會更新為圖片的實際高度。 Compose 會安排下一個畫面的重組作業。在繪圖階段 由於不會反映值變更,因此文字會以邊框間距為 0 的方式呈現 。

接著,Compose 會依據 值變更值,安排啟動第二個畫面 imageHeightPx。系統會在 Box 內容區塊中讀取狀態,並叫用該狀態 建構階段這次,文字會帶有邊框間距 對應圖片的高度在版面配置階段,程式碼會設定 imageHeightPx 會再次執行,但由於值之後不會安排重組作業 保持不變

最後,我們取得文字所需的邊框間距,但並非最佳的做法 會花費一個額外的影格將邊框間距值傳回至其他階段 就會產生內容重疊的畫面

這個範例可能看起來很牽強,但請留意這個一般模式:

  • Modifier.onSizeChanged()onGloballyPositioned() 或其他版面配置 作業套件
  • 更新某個狀態
  • 使用該狀態做為版面配置修飾符的輸入內容 (padding()height() 或 相似)
  • 可能會重複執行

如要修正上述範例,只要使用正確的版面配置原始物件即可。範例 以上,使用簡單的 Column() 即可實作,不過 需要自訂的複雜範例,因此需要編寫 自訂版面配置請參閱自訂版面配置指南 瞭解詳情

這裡的一般原則是為多個 UI 提供單一可靠資料來源 元素之間需要測量和放置的位置。使用 適當的版面配置原始物件或建立自訂版面配置後, 共用父項可做為可靠資料來源 能在多個元素之間來回切換導入動態狀態會打破這項原則。