可組合項的生命週期

在這個頁面中,您將瞭解可組合項的生命週期,以及 Compose 如何決定可組合項是否需要重組。

生命週期總覽

管理狀態說明文件中所述,「組合」指的是應用程式的使用者介面,這個介面是透過執行可組合項所產生。Composition是描述您的UI的composable的樹狀結構。

當 Jetpack Compose 首次執行您的composable時,在初始composition期間,系統會追蹤您呼叫的composable,以在Composition內容中描述您的 UI。等到應用程式的狀態變更時,Jetpack Compose 就會安排重新composition。重新安排時,Jetpack Compose 重新執行可能為改變狀態而變更的composable,然後更新Composition以反映任何變更。

組合只能由初始組合產生,並透過重組程序進行更新。修改組合的唯一方式是透過重新組合。

顯示composable生命週期的圖表

圖 1。 Composition內Composable的生命週期。而這會進入Composition狀態,被compose 0 次以上,然後留下Composition的內容。

一般而言,您更改 State<T> 物件後才會觸發重新composition。然後Compose追蹤這些composable,並在Composition內容中讀取這個特定 State<T> 的composable,以及無法呼叫的可略過內容。

如果多次呼叫某個可組合項,系統就會將多個執行個體置入組合中。每個呼叫在Composition內容中都有各自的生命週期。

@Composable
fun MyComposable() {
    Column {
        Text("Hello")
        Text("World")
    }
}

呈現前程式碼片段中元素階層的圖表

圖 2. 代表Composition的 MyComposable。如果多次呼叫某個可組合項,系統就會將多個執行個體置入組合中。不同顏色的元素代表獨立的執行個體。

Composition的composable的解剖

Composition的例項是由其呼叫網站識別。Compose 編譯器會將每個呼叫網站視為具有差異。呼叫多個呼叫網站的composable,用於建立Composition的多個composable執行個體。

如果在重新組合期間,可組合項呼叫的可組合項與先前組合時不同,Compose 會找出哪些可組合項已呼叫或未呼叫,而對於在兩個組合中都呼叫的可組合項,Compose 會避免重新組合這些可組合項,如果這些可組合項的輸入內容未變更

保留身分是將副作用與composable建立關聯在一起的重要關係,這樣使用者才能順利完成工作,不必每次都開始重新composition。

請參考以下範例:

@Composable
fun LoginScreen(showError: Boolean) {
    if (showError) {
        LoginError()
    }
    LoginInput() // This call site affects where LoginInput is placed in Composition
}

@Composable
fun LoginInput() { /* ... */ }

@Composable
fun LoginError() { /* ... */ }

在上方的程式碼片段中,LoginScreen 會以條件方式呼叫 LoginError composable,並且一律呼叫 LoginInput composable。每個呼叫都有專屬的呼叫網站和來源位置,編譯器將會用來識別該網路。

這張圖表顯示如何在 ShowError 旗標變更為 true 時重新編寫上述程式碼。新增了 LoginError composable,但其他不可調的元件則未重新composable。

圖 3。 當狀態發生變更和重新composition時,composable中的 LoginScreen 代表。相同顏色則表示未經過重新composition。

雖然系統會優先呼叫 LoginInput,但第二個呼叫仍會保留,但 LoginInput 執行個體會留存在composition中。此外,由於 LoginInput 的整個參數在整個參數中都發生過變更,因此 Compose 會略過對 LoginInput 的呼叫。

新增其他資訊以協助智慧重新設定

多次呼叫合成composition也會多次加到composition中。多次從同一呼叫位置呼叫可組合項時,Compose 並沒有任何資訊可供識別該可組合項的每次呼叫,因此除了呼叫位置,系統還會使用執行順序來區別這些執行個體。這類行為有時是必要的,但在某些情況下可能會產生不必要的行為。

@Composable
fun MoviesScreen(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            // MovieOverview composables are placed in Composition given its
            // index position in the for loop
            MovieOverview(movie)
        }
    }
}

在上述範例中,除了呼叫網站之外,Compose 還會使用執行順序,讓執行個體在composable中保持不同。如果在清單「底部」加入新的 movie,Compose 可重複使用已經存在組合中的執行個體,這是因為清單中的執行個體位置並未變更,因此這些執行個體都會輸入相同的 movie

這張圖表顯示在清單底部加入新元素時,前面的程式碼的重組方式。這份清單中的其他項目並未變更排名,也不會建議重新compose。

圖 4。. 在清單底部加入新元素時,代表composition中的 MoviesScreen。Composition中的 MovieOverview composable可以重複使用。在 MovieOverview 中使用相同的顏色,表示可組合的未重新composable。

不過,如果因為在清單「頂端」或「中間」新增項目,或是因為移除/重新排序項目而使得 movies 清單出現變化,那麼只要 MovieOverview 呼叫內含會變更清單內位置順序的輸入參數,就會啟動重組程序。舉例來說,如果 MovieOverview 使用副作用擷取電影圖片非常重要如果在動作執行期間進行重組,系統就會取消並重新開始。

@Composable
fun MovieOverview(movie: Movie) {
    Column {
        // Side effect explained later in the docs. If MovieOverview
        // recomposes, while fetching the image is in progress,
        // it is cancelled and restarted.
        val image = loadNetworkImage(movie.url)
        MovieHeader(image)

        /* ... */
    }
}

這張圖表顯示瞭如果將新的元素加到清單頂端,程式碼的重compose的方式。清單中的每個其他項目都會變更位置,因此必須重新compose。

圖 5。在清單中加入新元素時,代表compose中的 MoviesScreen。無法重複使用 MovieOverview composable,且所有副作用都會重新啟動。MovieOverview 中的不同顏色表示composable的compose。

在理想情況下,我們希望將 MovieOverview 的執行個體視為連結至該執行個體的 movie 身分。如果我們重新排序電影清單,最好會重新排列composition樹狀結構中的執行個體,而不是將不同的 MovieOverview composable組成不同的電影執行個體。Compose 可讓您向執行階段指明您要用來識別樹狀結構中特定部分的值:key composable的值。

透過呼叫由可傳入的一或多個值所發出的鍵來包裝程式碼區塊,系統會將這些值合併,以在composable中識別該執行個體。key 的值不必「全域」重複,因為在呼叫網站的元件組合中,該值可以重複。在這個範例中,movie必須設有key這個數字在movies;同意分享key其他一些元件組合

@Composable
fun MoviesScreenWithKey(movies: List<Movie>) {
    Column {
        for (movie in movies) {
            key(movie.id) { // Unique ID for this movie
                MovieOverview(movie)
            }
        }
    }
}

如上所述,即使清單上的元素發生變更,Compose 也會辨識對 MovieOverview 的個別呼叫,且可以重複使用。

這張圖表顯示瞭如果將新的元素加到清單頂端,程式碼的重compose的方式。由於清單項目是以鍵識別,因此 Compose 就不是要重新編排這些項目,即使其位置有所變更也一樣。

圖 6。在清單中加入新元素時,代表compose中的 MoviesScreen。由於 MovieOverview 元件含有專屬金鑰,因此 Compose 可辨識哪些 MovieOverview 執行個體並未變更,且可重複使用。他們的副作用會繼續執行。

某些composable已內建 key composable。舉例來說,LazyColumn 接受在 items DSL 中指定自訂 key

@Composable
fun MoviesScreenLazy(movies: List<Movie>) {
    LazyColumn {
        items(movies, key = { movie -> movie.id }) { movie ->
            MovieOverview(movie)
        }
    }
}

如果輸入內容未變更,則可略過

在重新組合期間,如果某些符合資格的可組合函式輸入內容與先前的組合內容相同,系統可以完全略過執行作業。

可組合函式只有在下列情況下才可略過:

  • 函式具有非 Unit 的傳回類型
  • 這個函式會加上 @NonRestartableComposable@NonSkippableComposable 註解
  • 必要參數的類型不穩定

我們有一個實驗性的編譯器模式,名為「Strong Skipping」,可放寬最後一項要求。

類型必須符合下列合約規範,才能視為穩定類型:

  • 兩個執行個體的 equals 結果「一律」會與兩個相同執行個體的結果相同。
  • 如果類型的公開資源有所異動,系統會通知Composition。
  • 所有公開資源類型的類型都是穩定版本。

合約中,Compose 編譯器會將幾個較常使用的類型視為穩定,即使使用 @Stable 註解並未明確將定義視為穩定版:

  • 所有原始值類型:BooleanIntLongFloatChar 等。
  • 字串
  • 所有函式類型 (lambdas)

所有這類型均能遵循穩定版的合約,因為這些變更無法變更。由於不可變更的類型一律不會改變,因此他們不用費心告知變更內容,因此您可以輕鬆遵循合約內容。

其中一個固定類型是穩定版,但可以成為 Compose 的 MutableState 類型。如果將值保存在MutableState,整體狀態物件會視為穩定,因為當使用者撰寫程式碼變更時, .value屬性State的 Google Ads 新帳戶重新申請驗證。

當所有已傳遞為組合的參數類型保持穩定時,系統會根據 UI 樹狀結構中的composable位置,比較參數值。如果上次呼叫後所有值都未變更,則系統會略過重新 composition。

只有在能夠證明類型時,Compose 才會認定其類型穩定。舉例來說,介面通常被視為不穩定,而包含可變動公開屬性的類型,在實作中可能無法變更。

如果 Compose 無法推論類型穩定,但您想強制 Compose 判定為穩定,請使用 @Stable 註解標記。

// Marking the type as stable to favor skipping and smart recompositions.
@Stable
interface UiState<T : Result<T>> {
    val value: T?
    val exception: Throwable?

    val hasError: Boolean
        get() = exception != null
}

在上方的程式碼片段中,由於 UiState 是介面,因此 Compose 通常可能會判定此類型不穩定。新增 @Stable 註解即表示 Compose 這個類型很穩定,以便 Compose 支援智慧重新composition。也就是說,當介面做為參數類型使用時,Compose 會將所有實作項目視為穩定版。