使用 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 是 這個物件提供三個 CompositionLocal 執行個體:顏色、字體排版 和形狀—讓您可以之後在 樂曲。具體來說,這些是 LocalColorsLocalShapes 和 您可以透過 MaterialTheme 存取的 LocalTypography 資源 colorsshapestypography 屬性。

@Composable
fun MyApp() {
    // Provides a Theme whose values are propagated down its `content`
    MaterialTheme {
        // New values for colors, 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.colors.primary
    )
}

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

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

舉例來說,LocalContentAlpha CompositionLocal 包含用於文字的偏好內容 Alpha 值, 圖示,藉此強調或減少強調 UI 不同部分。在 以下範例:CompositionLocalProvider 是用來提供不同的 組合中不同部分的值

@Composable
fun CompositionLocalExample() {
    MaterialTheme { // MaterialTheme sets ContentAlpha.high as default
        Column {
            Text("Uses MaterialTheme's provided alpha")
            CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
                Text("Medium value provided for LocalContentAlpha")
                Text("This Text also uses the medium value")
                CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
                    DescendantExample()
                }
            }
        }
    }
}

@Composable
fun DescendantExample() {
    // CompositionLocalProviders also work across composable functions
    Text("This Text uses the disabled alpha 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可透過組合向下傳遞資料的工具 隱含

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

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

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

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

決定是否要使用「CompositionLocal

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

CompositionLocal 應該有正確的預設值。如果沒有預設值 因此必須確保開發人員 會發生在沒有提供 CompositionLocal 值的情況下。 如未提供預設值,可能會在建立時引發問題和困擾 以測試或預覽使用該 CompositionLocal 的可組合函式 這需要明確提供

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

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

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

建立 CompositionLocal

有兩個 API 可用來建立CompositionLocal

  • compositionLocalOf: 變更在重組期間提供的值,只會失效 也就是讀取其 current 值。

  • staticCompositionLocalOf: 與 compositionLocalOf 不同,staticCompositionLocalOf 的讀取並非 此功能。變更此值會導致整個 content lambda ,提供 CompositionLocal 進行重組,而不是 而是在組成中讀取 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
    Card(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()
    }
}