使用 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 建立關聯。存取 CompositionLocalcurrent 資源時,CompositionLocalProvidercontent lambda 會取得提供的值。新值提供時,Compose 會重新編寫讀取 CompositionLocal 的 Composition 部分。

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

@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 可組合項的預覽畫面。

在上一例中,Material 可組合項在內部使用了 CompositionLocal 例項。如要存取 CompositionLocal 目前的值,請使用其 current 屬性。在以下範例中,我們可以看到 Android 應用程式常用的 LocalContext CompositionLocal,其中目前的 Context 值會用於設定文字格式:

@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可透過組合向下傳遞資料的工具 隱含

使用 CompositionLocal 的另一個關鍵信號是 跨切割和中階實作層不應瞭解 存在,因為讓這些中間層感知會限制 可組合項的公用程式。例如,若要查詢 Android 權限, 當時由 CompositionLocal 提供。媒體選擇器可組合項 可以在 Google Cloud 控制台中新增功能 而無需變更其 API,且需要媒體選擇器的呼叫端才能 但請注意環境中使用的額外情境

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

CompositionLocal 會導致可組合項的行為難以理解。阿斯 會建立隱含的依附元件,也就是使用這些依附元件的可組合項呼叫端 可確保滿足每個 CompositionLocal 的值。

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

決定是否要使用 CompositionLocal

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

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

如果您的設計概念屬於非「樹狀範圍或子階層範圍」的概念,請避免使用 CompositionLocalCompositionLocal 在所有子系 (而非只有少數) 都能使用的情況下才有意義。

如果您的用例不符合上述要求,請先參閱「可以考慮改用的替代方案」一節,然後再建立 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 所提供的值 (為該 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 的職責可能不只一個。另外, 將 MyViewModel 做為依附元件傳遞會使 MyDescendant 更容易重複使用,因為 這些元素現在已結合建議您改用不會傳遞 對子系的依賴並使用反轉的控制原則 讓祖系負責執行邏輯:

@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()
    }
}