修正穩定性問題

如果遇到會導致效能問題的不穩定類別,您應將其設為穩定。本文件將概述您可以用來執行這項操作的幾種技巧。

啟用嚴格略過功能

建議您先嘗試啟用嚴格略過模式。嚴格略過模式可略過含有不穩定參數的可組合項,也是修正穩定性問題所造成的效能問題最簡單的方法。

詳情請參閱強制略過

讓類別不可變更

您也可以嘗試讓不穩定的類別完全不可變動。

  • 不可變動:表示在建構該類型的執行個體之後,任何屬性的值都不能變更,且所有方法在明確情況下都是透明的。
    • 請確認所有類別屬性都是 val 而非 var,且屬性類型為不可變型。
    • String, IntFloat 等原始類型一律無法變更。
    • 如果無法採取這種做法,您必須為任何可變動屬性使用 Compose 狀態。
  • 穩定版:表示可變動的類型。Compose 執行階段不知道該類型任何類型的公開屬性或方法行為是否會產生與先前叫用所產生的不同結果。

不可變更的集合

Compose 認為類別不穩定的原因為集合。如「診斷穩定性問題」頁面所述,Compose 編譯器無法完全確保 List, MapSet 等集合無法完全變更,因此會將這些集合標示為不穩定。

如要解決這個問題,您可以使用不可變動的集合。Compose 編譯器支援 Kotlinx 不可變集合。這些集合保證無法變更,而且 Compose 編譯器會據此處理。此程式庫目前仍在 Alpha 測試階段,因此預期 API 可能會有異動。

請在「診斷穩定性問題」指南中再次檢視這個不穩定的類別:

unstable class Snack {
  …
  unstable val tags: Set<String>
  …
}

您可以使用不可變動的集合,將 tags 設為穩定版。在該類別中,將 tags 的類型變更為 ImmutableSet<String>

data class Snack{
    …
    val tags: ImmutableSet<String> = persistentSetOf()
    …
}

這樣做之後,該類別的所有參數都無法變更,而 Compose 編譯器會將類別標示為穩定版。

使用 StableImmutable 註解

解決穩定性問題的可能方法,是使用 @Stable@Immutable 為不穩定的類別加上註解。

為類別加註將覆寫編譯器原本會「推論」類別的哪些內容。這與 Kotlin 中的 !! 運算子類似。使用這些註解時 請務必謹慎小心覆寫編譯器行為可能會導致非預期的錯誤,例如可組合函式在預期情況下沒有重組。

如果可以在沒有註解的情況下讓類別保持穩定,您應該設法達成穩定性。

以下程式碼片段提供資料類別的簡單範例,此類別已註解為不可變動:

@Immutable
data class Snack(
…
)

無論您使用 @Immutable@Stable 註解,Compose 編譯器都會將 Snack 類別標示為穩定。

集合中的註解類別

假設可組合函式包含 List<Snack> 類型的參數:

restartable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  …
  unstable snacks: List<Snack>
  …
)

即使您使用 @ImmutableSnack 加上註解,Compose 編譯器仍會將 HighlightedSnacks 中的 snacks 參數標示為不穩定。

在集合類型方面,參數會遇到與類別相同的問題,Compose 編譯器一律會將 List 型別的參數標示為不穩定,即使是穩定型別的集合也是如此。

您無法將個別參數標示為穩定版,也不能為可組合項加註一律可略過。有幾種方法可以前進。

有幾種方式可以解決集合不穩定的問題。以下各小節將概略說明這些不同做法。

設定檔

如果您願意遵守程式碼集的穩定性合約,可以在穩定性設定檔中新增 kotlin.collections.*,選擇將 Kotlin 集合視為穩定。

不可變集合

為了維持編譯時間的不變性,您可以使用 kotlinx 的不可變集合,而不使用 List

@Composable
private fun HighlightedSnacks(
    …
    snacks: ImmutableList<Snack>,
    …
)

Wrapper

如果您無法使用不可變更的集合,可以自行建立集合。如要這麼做,請將 List 納入註解的穩定類別中。根據您的需求,一般包裝函式可能是最佳選擇。

@Immutable
data class SnackCollection(
   val snacks: List<Snack>
)

並在可組合函式中,將其做為參數類型。

@Composable
private fun HighlightedSnacks(
    index: Int,
    snacks: SnackCollection,
    onSnackClick: (Long) -> Unit,
    modifier: Modifier = Modifier
)

解決方案

採用這兩種方法後,Compose 編譯器現在會將 HighlightedSnacks 可組合項標示為 skippablerestartable

restartable skippable scheme("[androidx.compose.ui.UiComposable]") fun HighlightedSnacks(
  stable index: Int
  stable snacks: ImmutableList<Snack>
  stable onSnackClick: Function1<Long, Unit>
  stable modifier: Modifier? = @static Companion
)

在重組期間,如果 HighlightedSnacks 的輸入都沒有變更,Compose 現在可以略過 HighlightedSnacks

穩定性設定檔

從 Compose Compiler 1.5.5 開始,可以在編譯期間提供類別,讓系統認為該類別的穩定性。這可讓您在不控管的類別時,將這類類別視為穩定版,例如 LocalDateTime 等標準程式庫類別。

設定檔為純文字檔案,每行一個類別。支援註解、單一和雙萬用字元。設定範例如下:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider my datalayer stable
com.datalayer.*
// Consider my datalayer and all submodules stable
com.datalayer.**
// Consider my generic type stable based off it's first type parameter only
com.example.GenericClass<*,_>

如要啟用這項功能,請將設定檔路徑傳遞至 Compose 編譯器 Gradle 外掛程式設定的 composeCompiler 選項區塊。

composeCompiler {
  stabilityConfigurationFile = rootProject.layout.projectDirectory.file("stability_config.conf")
}

由於 Compose 編譯器會在專案中的每個模組上個別執行,因此您可以視需要為不同的模組提供不同的設定。或者,您也可以在專案的根層級中設定一個設定,並將該路徑傳遞給每個模組。

多個模組

另一個常見問題涉及多模組架構。只有在所參照的所有非原始類型都明確標示為穩定版,或是透過 Compose 編譯器建構的模組中,Compose 編譯器才能推斷某個類別是否穩定。

如果資料層位於 UI 層的不同模組中 (這是建議做法),這可能會導致問題。

解決方案

如要解決這個問題,您可以採取下列任一做法:

  1. 新增類別至編譯器設定檔
  2. 在資料層模組上啟用 Compose 編譯器,或視情況使用 @Stable@Immutable 標記類別。
    • 這包括在資料層中新增 Compose 依附元件。然而,這只是 Compose 執行階段的依附元件,而非 Compose-UI 的依附元件。
  3. 在 UI 模組中,將資料層類別包裝在特定 UI 的包裝函式類別中。

如果使用外部程式庫但未使用 Compose 編譯器,也會發生相同的問題。

並非所有可組合項都應可略過

修正穩定性問題時,請不要將所有可組合元件設為可略過。這麼做可能會導致過早的最佳化,導致的問題比解決的問題還多。

在許多情況下,可略過的功能並沒有任何實際好處,反而可能導致程式碼難以維護。例如:

  • 不常或完全不重組的可組合函式。
  • 本身只會呼叫可略過的可組合函式。
  • 具有大量參數的可組合項,這些參數的費用等於實作。在這種情況下,檢查是否有任何參數變更的成本,可能會超過低成本重組的成本。

當可組合項可略過時,會增加一些額外負擔,這可能不值得。如果您認為可重新啟動的負擔高於其價值,您甚至可以將可組合項加註為無法重新啟動