修正穩定性問題

如果遇到導致效能問題的不穩定類別,請將其設為穩定。本文將概述幾種可用的方法。

啟用嚴格略過模式

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

詳情請參閱強制略過

讓類別不可變更

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

  • 不可變動:表示類型,其中任何屬性的值在建構該類型的執行個體後,就永遠無法變更,且所有方法在參照上都是透明的。
    • 請確認類別的所有屬性都是 val 而非 var, 且屬於不可變動的型別。
    • String, IntFloat 等原始型別一律為不可變動。
    • 如果無法這麼做,您必須針對任何可變動的屬性使用 Compose 狀態。
  • 穩定:表示可變動的型別。如果任何型別的公開屬性或方法行為會產生與先前叫用不同的結果,Compose 執行階段不會察覺。

不可變更的集合

Compose 將類別視為不穩定的常見原因為集合。如「診斷穩定性問題」頁面所述,Compose 編譯器無法完全確定 List, MapSet 等集合是否真的不可變動,因此會將其標示為不穩定。

如要解決這個問題,可以使用不可變動的集合。Compose 編譯器支援 Kotlinx Immutable Collections。這些集合保證不可變動,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>
  
)

即使使用 @Immutable 註解 Snack,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 編譯器 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 編譯器,也會發生相同問題。

並非每個可組合函式都應可略過

修正穩定性問題時,請勿嘗試讓每個可組合函式都可略過。如果過早進行最佳化,可能會導致問題越修越多。

在許多情況下,可略過性不會帶來任何實質好處,反而可能導致難以維護的程式碼。例如:

  • 不會或很少重組的可組合項。
  • 可組合函式本身只會呼叫可略過的可組合函式。
  • 具有大量參數的可組合項,且實作成本高昂。在這種情況下,檢查是否有任何參數變更的成本,可能會超過重新組合的成本。

可略過的可組合項會增加少量負擔,可能不值得。如果判斷可重新啟動的成本高於價值,您甚至可以將可組合函式註解為「不可重新啟動」