修正穩定性問題

遇到導致效能問題的不穩定類別時,應保持穩定。本文件將概略說明可用於達成這個目的的幾種技巧。

啟用明確略過功能

您應該先嘗試啟用強式略過模式。強式略過模式可略過含有不穩定參數的可組合項,這是修正穩定性造成效能問題最簡單的方法。

詳情請參閱強式略過功能

將類別設為不可變動

您也可以嘗試將不穩定的類別設為完全不可變的類別。

  • 「不可變」:表示該類型,在建構該類型的例項後,任何屬性的值都不可變更,且所有方法皆為極透明。
    • 確認類別的所有屬性皆為 val (而非 var),以及不可變動的類型。
    • String, IntFloat 等原始類型一律不可變動。
    • 如果無法這麼做,您就必須針對任何可變動屬性使用 Compose 狀態。
  • 穩定版:表示可變動的類型。如果類型的任何公開屬性或方法行為會產生與之前叫用不同的結果,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
)

在重組期間,如果沒有任何輸入內容變更,Compose 現在可略過 HighlightedSnacks

穩定性設定檔

從 Compose Compiler 1.5.5 開始,您可以在編譯期間提供類別,做為穩定版的設定檔。這可將您未控制的類別 (例如 LocalDateTime 等標準程式庫類別) 視為穩定版。

設定檔為純文字檔案,每列有一個類別。系統支援註解、單一及雙萬用字元。設定範例如下所示:

// Consider LocalDateTime stable
java.time.LocalDateTime
// Consider kotlin collections stable
kotlin.collections.*
// 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 編譯器選項。

Groovy

kotlinOptions {
    freeCompilerArgs += [
            "-P",
            "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
                    project.absolutePath + "/compose_compiler_config.conf"
    ]
}

Kotlin

kotlinOptions {
  freeCompilerArgs += listOf(
      "-P",
      "plugin:androidx.compose.compiler.plugins.kotlin:stabilityConfigurationPath=" +
      "${project.absolutePath}/compose_compiler_config.conf"
  )
}

Compose 編譯器會分別在專案中的每個模組上執行,因此您可以視需要為不同模組提供不同的設定。或者,您也可以在專案的根層級進行一項設定,然後將該路徑傳送至各個模組。

多個模組

另一個常見問題牽涉到多模組架構。只有在該類別參照的所有非原始類型已明確標示為穩定,或是由 Compose 編譯器建構的模組中時,Compose 編譯器才能推論該類別是否穩定。

如果資料層位於與 UI 層的獨立模組中 (此為建議做法),就可能會發生這個問題。

解決方案

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

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

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

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

修正穩定性問題時,請勿嘗試讓每個可組合項可略過。嘗試這樣做可能會導致提前最佳化,而問題可能超出修正範圍。

在許多情況下,可略過設定並不會帶來任何實質效益,因而導致難以維護程式碼。例如:

  • 不會經常或根本重組的可組合項。
  • 本身只會呼叫可略過的可組合項。
  • 具有大量參數且價格昂貴實作的可組合項。在這種情況下,檢查任何參數是否已變更的費用,可能會超過節省重組費用的成本。

可略過的可組合項會增加小額負荷,可能不值得。如果您發現可重新啟動的負擔高於自身價值,也可以將可組合項的註解設為「無法重新啟動」