[Android]使用CompositionLocal隐式传值

1.相关概念

CompositionLocal 是定义数据的方式,而 CompositionLocalProvider 是在 Compose UI 树中传递这些数据的工具。二者合作,为 Compose 应用提供了一个强大的状态和数据流管理机制,使得数据可以在组件间按需传递,而无需通过复杂的层级传递或全局状态。

这种模式非常适合于主题、语言偏好、UI配置等的全局管理,极大地简化了复杂应用中的数据传递和状态管理问题。

CompositionLocal

CompositionLocal 是一个特殊的类型,用于定义和存储可以在 Compose UI 树中访问的数据。你可以将其视为一个可在组件间传递的容器,它携带数据。创建 CompositionLocal 时,你通常会提供一个默认值,这是为了在没有明确提供值的上下文中使用。

例如:

Kotlin 复制代码
val LocalExampleData = compositionLocalOf { "Default Value" }

这行代码定义了一个 CompositionLocal 对象,其默认值为 "Default Value"CompositionLocal 本身不传递任何数据;它仅定义了数据的类型和默认值。

CompositionLocalProvider

CompositionLocalProvider 是一个 Composable 函数,用来在 Compose 的 UI 树中的某个点提供 CompositionLocal 的值。这个函数允许你在其作用域内覆盖 CompositionLocal 的值,从而所有在此作用域内的 Composable 函数都可以访问到这个新值。

例如,如果你想在特定的 UI 部分中使用不同的数据,你可以这样做:

Kotlin 复制代码
CompositionLocalProvider(LocalExampleData provides "Special Value") {
    // 这里的 Composable 函数可以使用 "Special Value"
    Text(
        text = LocalExampleData.current,
    )
}

在这个作用域内,任何访问 LocalExampleData 的 Composable 都将获得 "Special Value" 而不是默认值。

2.设置CompositionLocal的默认值

为什么需要默认值?

默认值的主要目的是提供一个后备值,这样当数据没有在上游通过 CompositionLocalProvider 显式提供时,组件仍然可以正常访问一个有效的值。这是一种防止应用在运行时因为缺少所需数据而崩溃的安全措施。

如何定义 CompositionLocal

如果您想创建一个 CompositionLocal,您必须在声明时提供一个默认值。

Kotlin 复制代码
val LocalExampleData = compositionLocalOf { "Default Value" }

如何让创建时不给定默认值?

如果您的设计中需要在某个点后才确定 CompositionLocal 的值,而又不想在一开始就给出一个具体的默认值,您可以考虑以下几种方法:

(1).使用可空类型

您可以将 CompositionLocal 的默认值设置为 null,这表示在没有提供值的情况下,默认值是 null。然后,您可以在适当的时候通过 CompositionLocalProvider 提供具体的值。

Kotlin 复制代码
val LocalExampleData = compositionLocalOf<String?> { null }
(2).使用哨兵值或逻辑检查

如果 null 对于您的应用场景不合适,您也可以使用一个特殊的值作为默认值,或者在使用时添加逻辑检查。

Kotlin 复制代码
val LocalExampleData = compositionLocalOf { "UNINITIALIZED" }

然后,在使用时检查这个值:

Kotlin 复制代码
@Composable
fun ExampleComponent() {
    val data = LocalExampleData.current
    if (data != "UNINITIALIZED") {
        Text("Data is: $data")
    } else {
        Text("Data is not initialized")
    }
}
(3).使用 error("...")

在 Jetpack Compose 中,使用 error("reason")compositionLocalOf 初始化时提供一个默认值,实际上是一种确保开发者必须在使用该 CompositionLocal 前明确提供一个值的策略。

使用 error("...") 的原因

主要原因是确保 CompositionLocal 没有被错误或未预期地使用,同时没有提供必要的值。这通常适用于以下情况:

  • 明确性和安全性 :通过抛出错误,开发者在开发过程中就能立即发现问题,而不是在应用运行过程中遇到不明确的行为或难以追踪的错误。这样可以确保所有使用这个 CompositionLocal 的组件都能获得正确的数据。

  • 严格的依赖管理 :这种方式强迫开发者在使用该 CompositionLocal 的组件中,通过 CompositionLocalProvider 明确地提供一个值。它减少了对默认值的依赖,使得组件的数据流更加清晰和可控。

  • 无合适默认值 :在某些情况下,可能没有一个合适的默认值可用。比如,在主题或配置特定的情况下,使用通用的默认值可能不适合所有使用场景。在这种情况下,使用 error() 可以避免不恰当的默认设置。

实例解释

Kotlin 复制代码
data class CustomColors(
    val textColor: androidx.compose.ui.graphics.Color
)

val LocalCustomColors = compositionLocalOf<CustomColors> { 
    error("No CustomColors provided") 
}

@Composable
fun MyApp() {
    CompositionLocalProvider(LocalCustomColors provides CustomColors(Color.Red)) {
        // 在这个作用域内,LocalCustomColors有具体的值
        ShowText()
    }
    // 如果在这里调用 ShowText(),将会抛出异常
}

@Composable
fun ShowText() {
    val colors = LocalCustomColors.current
    Text(text = "Hello, Compose!", color = colors.textColor)
}

3. 直接访问CompositionLocal默认值

如果你只定义了 CompositionLocal 而没有使用 CompositionLocalProvider 来覆盖它的值,你可以直接访问 CompositionLocal 中定义的默认值。

Kotlin 复制代码
val LocalExampleData = compositionLocalOf { "Default Value" }

@Composable
fun DisplayData() {
    val data = LocalExampleData.current
    Text(text = "Data is: $data")
}

@Composable
fun App() {
    DisplayData() // 这里会显示 "Data is: Default Value"
}

注意事项

尽管可以直接访问 CompositionLocal 的默认值,但实际上这种方式通常不是最佳实践,除非这个默认值确实是你在大部分情况下所需要的。通常,CompositionLocal 被设计用来在 UI 树的不同层级中传递动态数据或配置,其默认值作为一种安全的回退。

4.**CompositionLocal+**CompositionLocalProvider 的基本使用

  • 定义 CompositionLocal :首先定义一个 CompositionLocal。这是一个可以在组件树中传递的数据容器。

  • 使用 CompositionLocalProvider 设置值 :在组件树的适当位置,使用 CompositionLocalProvider 来注入数据。

  • 在组件中访问这些值 :在子组件中,你可以直接访问这些通过 CompositionLocal 提供的数据。

传递单个值

下面是一个具体的例子,展示了如何使用 CompositionLocalProvider 来传递主题信息.

Kotlin 复制代码
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocal
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.material.Text
import androidx.compose.material.MaterialTheme
import androidx.compose.ui.graphics.Color

// 步骤 1: 定义一个 CompositionLocal,用于存储颜色
val LocalCustomTextColor = compositionLocalOf { Color.Black }

@Composable
fun App() {
    // 步骤 2: 使用 CompositionLocalProvider 提供颜色值
    CompositionLocalProvider(LocalCustomTextColor provides Color.Red) {
        // 步骤 3: 在子组件中使用这个颜色
        Greeting("Android")
    }
}

@Composable
fun Greeting(name: String) {
    // 在 Text 组件中使用 CompositionLocal 中的颜色
    Text(
        text = "Greetings, $name",
        color = LocalCustomTextColor.current // 直接访问颜色
    )
}

传递多个值

如果想要在整个应用中传递和使用多个主题属性(如颜色、形状和字体),你可以定义多个 CompositionLocal 对象来存储这些属性。然后,使用 CompositionLocalProvider 在组件树中提供这些属性的值。

Kotlin 复制代码
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontFamily.Companion.Monospace
import androidx.compose.ui.text.font.FontFamily.Companion.SansSerif
import androidx.compose.ui.unit.dp

// 定义颜色
val LocalCustomTextColor = compositionLocalOf { Color.Black }

// 定义形状
val LocalCustomShape = compositionLocalOf { RoundedCornerShape(corner = CornerSize(4.dp)) }

// 定义字体
val LocalCustomFontFamily = compositionLocalOf { SansSerif }

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

        setContent {
            Column {
                // 步骤 2: 使用 CompositionLocalProvider 来设置这些属性的值
                CompositionLocalProvider(
                    LocalCustomTextColor provides Color.Red,
                    LocalCustomShape provides RoundedCornerShape(10.dp),
                    LocalCustomFontFamily provides Monospace
                ) {
                    // 步骤 3: 在子组件中使用这些值
                    Greeting("Android")
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    Box(
        modifier = Modifier
            .background(color = Color.Gray, shape = LocalCustomShape.current)
            .padding(16.dp)
    ) {
        Text(
            text = "Hello $name!",
            color = LocalCustomTextColor.current,
            fontFamily = LocalCustomFontFamily.current
        )
    }
}

5.嵌套使用 CompositionLocalProvider

这种嵌套使用 CompositionLocalProvider 的方法非常有用,特别是在你需要根据界面的不同部分调整样式或功能时。每个 CompositionLocalProvider 可以覆盖其父 CompositionLocalProvider 提供的值,从而实现精细化的控制。

假设你有一个全局的颜色主题,但在某个特定屏幕或组件中,你想要一个不同的颜色主题。你可以通过嵌套使用 CompositionLocalProvider 来实现这一点:

Kotlin 复制代码
// 定义一个 CompositionLocal 来存储颜色
val LocalCustomTextColor = compositionLocalOf { Color.Black }

@Composable
fun App() {
    // 全局设置为红色
    CompositionLocalProvider(LocalCustomTextColor provides Color.Red) {
        Greeting("Hello, Compose!")
        // 在特定区域覆盖为蓝色
        CompositionLocalProvider(LocalCustomTextColor provides Color.Blue) {
            SpecialSection()
        }
    }
}

@Composable
fun Greeting(name: String) {
    // 这里使用的是红色
    Text("Greetings, $name", color = LocalCustomTextColor.current)
}

@Composable
fun SpecialSection() {
    // 这里使用的是蓝色
    Text("Welcome to the special section", color = LocalCustomTextColor.current)
}
相关推荐
阿巴斯甜7 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker7 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq95278 小时前
Andorid Google 登录接入文档
android
黄林晴10 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android
张小潇1 天前
AOSP15 Input专题InputReader源码分析
android
_小马快跑_1 天前
Kotlin | 协程调度器选择:何时用CoroutineScope配置,何时用launch指定?
android