修正穩定性問題

面對會造成效能問題的不穩定類別時,您應保持穩定。本文件將概述可用來達成此目標的幾種技巧。

將類別設為不可變動

建議您先嘗試將不穩定的類別設為完全不可變動。

  • 不可變動:表示在建構該類型的執行個體之後,所有屬性的值都不得變更的類型,且所有方法都參照透明化。
    • 確認該類別的所有屬性都是 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
)

在重組期間,如果沒有任何輸入變更,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 編譯器才能推斷類別是否穩定。

如果資料層位於使用者介面層的獨立模組中 (此為建議做法),就可能會遇到這個問題。

解決方案

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

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

如果外部程式庫沒有使用 Compose 編譯器,也會發生同樣的問題。

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

嘗試修正穩定性相關問題時,請勿嘗試讓每個可組合項略過。嘗試這樣做可能會導致過早的最佳化作業,造成比修正更多的問題。

在許多情況下,略過廣告並不會產生實際效益,且可能會導致程式碼難以維護。例如:

  • 不經常或完全重組的可組合項。
  • 本身只會呼叫可略過的可組合項。
  • 具有大量參數且實作成本相等的可組合項。在本例中,檢查任何參數變更的成本都可能會超過便宜重組的成本。

可略過的可組合項會增加些微負擔,不見得值得。如果您判定可重新啟動的負擔比實際空間高,甚至可以將可組合項標註為不可重新啟動的註解。