使用 CompositionLocal 的本機範圍資料

CompositionLocal 是可透過 Composition 隱密傳遞資料的工具。在這個頁面中,您將進一步瞭解 CompositionLocal,我們也會說明您應如何建立自己的 CompositionLocal,以及 CompositionLocal 是否為您用例的理想解決方案。

隆重推出 CompositionLocal

通常在 Compose 中,資料會做為每個可組合函式的參數,透過 UI 樹狀結構向下傳遞。如此一來 明確依附依附元件不過,對使用率高且用途廣泛的資料 (例如顏色或類型樣式) 來說,這可能並不容易。請參閱以下範例:

@Composable
fun MyApp() {
    // Theme information tends to be defined near the root of the application
    val colors = colors()
}

// Some composable deep in the hierarchy
@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        color = colors.onPrimary // ← need to access colors here
    )
}

為避免需要將顏色做為顯式參數的依附元件傳遞給大多數可組合項,Compose 提供了 CompositionLocal,讓您建立以樹狀結構範圍命名的物件,進而透過隱含的方式讓資料通過 UI 樹狀結構。

系統通常會為 CompositionLocal 在 UI 樹狀結構的特定節點中提供一個值。該值可用在相關的可組合項子系中,而無須將 CompositionLocal 宣告為可組合函式中的參數。

CompositionLocal 是 Material 主題在內部使用的方式。MaterialTheme 這個物件提供了 3 個 CompositionLocal 例項:colorSchemetypographyshapes,方便您之後在 Composition 的任何子系部分進行擷取。具體來說,這些是 LocalColorSchemeLocalShapes 和 您可以透過 MaterialTheme 存取的 LocalTypography 資源 colorSchemeshapestypography 屬性。

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colorScheme, typography, and shapes are available
        // in MaterialTheme's content lambda.

        // ... content here ...
    }
}

// Some composable deep in the hierarchy of MaterialTheme
@Composable
fun SomeTextLabel(labelText: String) {
    Text(
        text = labelText,
        // `primary` is obtained from MaterialTheme's
        // LocalColors CompositionLocal
        color = MaterialTheme.colorScheme.primary
    )
}

CompositionLocal 執行個體的範圍限定為 Composition 的一部分,因此 可以在樹狀結構的不同層級提供不同的值。CompositionLocalcurrent 值會對應到祖系在 Composition 該部分中提供的最接近值。

如要為 CompositionLocal 提供新的值,請使用 CompositionLocalProvider 及相關的 provides 修正函式,後者可將 CompositionLocal 金鑰與 value 建立關聯。 CompositionLocalProvidercontent lambda 會取得提供的 值 (用於存取 CompositionLocalcurrent 屬性) 時。新值提供時,Compose 會重新編寫讀取 CompositionLocal 的 Composition 部分。

舉例來說,LocalContentColor CompositionLocal 包含偏好的內容顏色,用於文字和圖像,以確保與目前背景顏色形成對比。在以下範例中,我們將 CompositionLocalProvider 用來為 Composition 的不同部分提供不同的值。

@Composable
fun CompositionLocalExample() {
    MaterialTheme {
        // Surface provides contentColorFor(MaterialTheme.colorScheme.surface) by default
        // This is to automatically make text and other content contrast to the background
        // correctly.
        Surface {
            Column {
                Text("Uses Surface's provided content color")
                CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.primary) {
                    Text("Primary color provided by LocalContentColor")
                    Text("This Text also uses primary as textColor")
                    CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.error) {
                        DescendantExample()
                    }
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the error color now")
}

圖 1. CompositionLocalExample 可組合項的預覽畫面。

在最後一個範例中,CompositionLocal 執行個體是在內部使用 透過 Material Design 可組合項如要存取 CompositionLocal 目前的值, 使用 current 資源。在以下範例中,LocalContext 目前的 Context 值 Android 應用程式常用的 CompositionLocal 用於設定格式 例如:

@Composable
fun FruitText(fruitSize: Int) {
    // Get `resources` from the current value of LocalContext
    val resources = LocalContext.current.resources
    val fruitText = remember(resources, fruitSize) {
        resources.getQuantityString(R.plurals.fruit_title, fruitSize)
    }
    Text(text = fruitText)
}

自行建立 CompositionLocal

CompositionLocal可透過 Composition 隱密傳遞資料的工具

使用 CompositionLocal 的另一個關鍵信號是 跨切割和中階實作層不應瞭解 存在,因為讓這些中間層感知會限制 可組合項的公用程式。舉例來說,Android 權限的查詢作業是由內部的 CompositionLocal 負責提供。為了存取裝置上受權限保護的內容,媒體選擇器可組合項可以新增功能,而無需更改其 API,也無須要求媒體選擇器的呼叫者注意在環境中使用的這項新增內容。

不過,CompositionLocal 不一定是最合適的解決方案。三 不建議過度使用 CompositionLocal,因為其有一些缺點:

CompositionLocal 會導致可組合項的行為難以理解。當它建立隱含的依附元件時,可組合項的呼叫者必須確保滿足每個 CompositionLocal 的值。

此外,這個依附元件可能沒有明確的可靠資料來源 可以變更組合的任何部分。因此,在問題發生時對應用程式進行偵錯可能會較為困難,因為您需要回到 Composition 頂端查看提供 current 值的位置。提供工具,例如 Finder IDE 或 Compose 版面配置檢查器的用法提供了充分資訊, 有助於解決這個問題

決定是否要使用 CompositionLocal

在某些情況下,「CompositionLocal」非常適合用來解決問題 適合所需用途

CompositionLocal 應有正確的預設值。如果沒有預設值 因此必須確保開發人員 會發生在沒有提供 CompositionLocal 值的情況下。 建立測試或預覽使用該 CompositionLocal 的可組合項時,如未提供預設值,可能會導致問題和失敗,因此一律必須明確提供該值。

避免使用 CompositionLocal 表示不屬於樹狀結構範圍或 以子階層為範圍CompositionLocal 視情況而定 可能只用於任何子系,而非只有少數。

如果您的用途不符合這些規定,請參閱 請先參考替代方案一節,再建立 CompositionLocal

建立 CompositionLocal 來保留 特定畫面的 ViewModel,讓該畫面中的所有可組合項 取得 ViewModel 的參照以執行部分邏輯。這是不良做法,因為並非特定 UI 樹狀結構下的所有可組合項都需要瞭解 ViewModel。建議您只將資訊傳送至可組合項 需要遵循狀態向下流及事件上移的模式。這麼一來,您的可組合項將更容易重複使用,測試起來也會更簡單。

建立 CompositionLocal

有兩個 API 可用來建立 CompositionLocal

  • compositionLocalOf:更改在重組期間提供的值,「只會」使讀取其 current 值的內容失效。

  • staticCompositionLocalOf:與 compositionLocalOf 不同,Compose 不會追蹤 staticCompositionLocalOf 的讀取作業。變更此值會導致系統重新組合 content lambda 的整體性 (提供 CompositionLocal 的位置,而不只是在 Composition 中讀取 current 值的位置)。

如果提供給 CompositionLocal 的值不太可能變更, 一律保持不變,使用 staticCompositionLocalOf 來獲取效能優勢。

舉例來說,應用程式的設計系統可能有一貫模式:藉由 UI 元件的陰影提升可組合項的高度。由於應用程式的不同高度應在整個 UI 樹狀結構中傳播,因此我們使用 CompositionLocal。由於 CompositionLocal 值有條件衍生 根據系統主題,我們使用 compositionLocalOf API:

// LocalElevations.kt file

data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)

// Define a CompositionLocal global object with a default
// This instance can be accessed by all composables in the app
val LocalElevations = compositionLocalOf { Elevations() }

CompositionLocal 提供值

CompositionLocalProvider 可組合元件會根據指定的目標,將值繫結至 CompositionLocal 執行個體 階層如要為 CompositionLocal 提供新值,請使用 provides 修正函式,該函式可將 CompositionLocal 金鑰與 value 建立關聯,如下所示:

// MyActivity.kt file

class MyActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            // Calculate elevations based on the system theme
            val elevations = if (isSystemInDarkTheme()) {
                Elevations(card = 1.dp, default = 1.dp)
            } else {
                Elevations(card = 0.dp, default = 0.dp)
            }

            // Bind elevation as the value for LocalElevations
            CompositionLocalProvider(LocalElevations provides elevations) {
                // ... Content goes here ...
                // This part of Composition will see the `elevations` instance
                // when accessing LocalElevations.current
            }
        }
    }
}

使用 CompositionLocal

CompositionLocal.current 會傳回最接近的 CompositionLocalProvider 所提供的值 (CompositionLocalProvider 會為 CompositionLocal 提供值):

@Composable
fun SomeComposable() {
    // Access the globally defined LocalElevations variable to get the
    // current Elevations in this part of the Composition
    MyCard(elevation = LocalElevations.current.card) {
        // Content
    }
}

可以考慮改用的替代方案

對某些用例而言,CompositionLocal 是極端的解決方案。如果您的用例不符合「判斷是否要使用 CompositionLocal」一節中指定的條件,可能就更適合採用其他解決方案。

傳送明確的參數

明確表明可組合項的依附元件是個好習慣。建議做法 您「只」對可組合項傳遞所需項目。為了順利分解和重複使用可組合項,每個可組合項應盡可能減少儲存的資訊量。

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel.data)
}

// Don't pass the whole object! Just what the descendant needs.
// Also, don't  pass the ViewModel as an implicit dependency using
// a CompositionLocal.
@Composable
fun MyDescendant(myViewModel: MyViewModel) { /* ... */ }

// Pass only what the descendant needs
@Composable
fun MyDescendant(data: DataToDisplay) {
    // Display data
}

控制反轉

避免將不必要的依附元件傳遞至可組合項的另一個方法,是透過「控制反轉」而非子系將依附元件 會改為執行某些邏輯

請參閱下例,子系必須觸發要求, 載入一些資料:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    MyDescendant(myViewModel)
}

@Composable
fun MyDescendant(myViewModel: MyViewModel) {
    Button(onClick = { myViewModel.loadData() }) {
        Text("Load data")
    }
}

視情況而定,MyDescendant 的職責可能不只一個。此外,由於 MyDescendantMyViewModel 已配對,因此將 MyViewModel 做為依附元件傳送會增加重複使用的困難性。建議您改用不會傳遞 對子系的依賴並使用反轉的控制原則 讓祖系負責執行邏輯:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusableLoadDataButton(
        onLoadClick = {
            myViewModel.loadData()
        }
    )
}

@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
    Button(onClick = onLoadClick) {
        Text("Load data")
    }
}

這個方法更適合某些用途,因為將 來自其直接祖系的子項。為了提供較低層級的彈性可組合項,祖系可組合項通常會更為複雜。

同樣地,您可以透過相同方式使用 @Composable 內容 lambda,以享有相同的成果:

@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
    // ...
    ReusablePartOfTheScreen(
        content = {
            Button(
                onClick = {
                    myViewModel.loadData()
                }
            ) {
                Text("Confirm")
            }
        }
    )
}

@Composable
fun ReusablePartOfTheScreen(content: @Composable () -> Unit) {
    Column {
        // ...
        content()
    }
}