CompositionLocal 深度解析:Compose 中的隐式依赖传递

2024-05-10 · 24 min · 依赖注入

在 Compose 中,数据通常通过参数显式传递。但有些数据(如主题、语言环境、导航控制器)需要在整个组件树中共享,逐层传递会非常繁琐。CompositionLocal 提供了一种隐式传递数据的机制,让深层组件可以直接访问祖先提供的数据。

一、什么是 CompositionLocal?

CompositionLocal 是 Compose 中的隐式数据传递机制,类似于 React 的 Context 或依赖注入框架的作用域。

// 不使用 CompositionLocal:需要层层传递
@Composable
fun App(theme: AppTheme) {
    Screen(theme = theme)
}

@Composable
fun Screen(theme: AppTheme) {
    Card(theme = theme)
}

@Composable
fun Card(theme: AppTheme) {
    Text(color = theme.textColor, ...)  // 终于用到了!
}

// 使用 CompositionLocal:直接获取
@Composable
fun Card() {
    val theme = LocalAppTheme.current
    Text(color = theme.textColor, ...)
}

二、内置的 CompositionLocal

Compose 提供了许多内置的 CompositionLocal:

CompositionLocal 类型 用途
LocalContextContextAndroid Context
LocalConfigurationConfiguration设备配置
LocalDensityDensity像素密度,dp/px 转换
LocalLayoutDirectionLayoutDirection布局方向(LTR/RTL)
LocalLifecycleOwnerLifecycleOwner生命周期所有者
LocalContentColorColor当前内容颜色

使用内置 CompositionLocal

@Composable
fun DeviceInfo() {
    val context = LocalContext.current
    val configuration = LocalConfiguration.current
    val density = LocalDensity.current

    Column {
        Text("屏幕宽度: ${configuration.screenWidthDp}dp")
        Text("像素密度: ${density.density}")
        
        // dp 转 px
        val paddingPx = with(density) { 16.dp.toPx() }
        Text("16dp = ${paddingPx}px")
    }
}

三、创建自定义 CompositionLocal

staticCompositionLocalOf vs compositionLocalOf

Compose 提供两种创建方式:

// 1. staticCompositionLocalOf:值很少变化时使用
// 值变化时,整个使用该值的子树都会重组
val LocalAppConfig = staticCompositionLocalOf<AppConfig> {
    error("No AppConfig provided")
}

// 2. compositionLocalOf:值可能频繁变化时使用
// 值变化时,只有读取该值的 Composable 重组
val LocalUserPreferences = compositionLocalOf<UserPreferences> {
    UserPreferences()  // 默认值
}

💡 如何选择?

staticCompositionLocalOf:主题、配置等很少变化的数据
compositionLocalOf:用户偏好、动态设置等可能变化的数据

完整示例:自定义主题系统

// 1. 定义数据类
@Immutable
data class CustomColors(
    val primary: Color,
    val secondary: Color,
    val background: Color,
    val onBackground: Color
)

// 2. 创建 CompositionLocal
val LocalCustomColors = staticCompositionLocalOf<CustomColors> {
    error("No CustomColors provided")
}

// 3. 创建便捷访问对象
object CustomTheme {
    val colors: CustomColors
        @Composable
        @ReadOnlyComposable
        get() = LocalCustomColors.current
}

// 4. 创建主题 Provider
@Composable
fun CustomTheme(
    darkTheme: Boolean = isSystemInDarkTheme(),
    content: @Composable () -> Unit
) {
    val colors = if (darkTheme) darkColors else lightColors

    CompositionLocalProvider(
        LocalCustomColors provides colors
    ) {
        content()
    }
}

// 5. 使用自定义主题
@Composable
fun ThemedCard(title: String) {
    Card(backgroundColor = CustomTheme.colors.surface) {
        Text(
            text = title,
            color = CustomTheme.colors.onBackground
        )
    }
}

四、CompositionLocalProvider 的使用

嵌套覆盖

内层 Provider 可以覆盖外层的值:

@Composable
fun NestedProviders() {
    CompositionLocalProvider(LocalContentColor provides Color.Black) {
        Text("黑色文字")  // 黑色
        
        CompositionLocalProvider(LocalContentColor provides Color.Red) {
            Text("红色文字")  // 红色(覆盖外层)
        }
        
        Text("还是黑色")  // 黑色
    }
}

五、性能考量

staticCompositionLocalOf 的重组范围

val LocalCounter = staticCompositionLocalOf { 0 }

@Composable
fun Parent() {
    var counter by remember { mutableStateOf(0) }

    CompositionLocalProvider(LocalCounter provides counter) {
        // counter 变化时,整个 Child 子树都会重组!
        Child()
    }
}

compositionLocalOf 的精确重组

val LocalCounter = compositionLocalOf { 0 }

@Composable
fun Child() {
    // 不读取 LocalCounter,不会重组
    ExpensiveComponent()
    
    // 只有这里重组
    CounterDisplay()
}

@Composable
fun CounterDisplay() {
    Text("Counter: ${LocalCounter.current}")  // 只有这个重组
}

六、常见使用场景

1. 导航控制器

val LocalNavController = staticCompositionLocalOf<NavHostController> {
    error("No NavController provided")
}

@Composable
fun DeepNestedButton() {
    val navController = LocalNavController.current
    
    Button(onClick = { navController.navigate("detail/123") }) {
        Text("查看详情")
    }
}

2. 用户会话

@Immutable
data class UserSession(
    val userId: String?,
    val isLoggedIn: Boolean,
    val permissions: Set<Permission>
)

val LocalUserSession = compositionLocalOf {
    UserSession(null, false, emptySet())
}

七、CompositionLocal vs 其他方案

方案 适用场景 优点 缺点
参数传递 少量层级、明确依赖 显式、易追踪 层级多时繁琐
CompositionLocal 跨多层级的共享数据 简洁、自动传递 隐式依赖、难追踪
ViewModel 屏幕级状态、业务逻辑 生命周期感知 需要 Hilt 等 DI

八、常见陷阱

1. 忘记提供 Provider

// ❌ 没有默认值,也没有 Provider,运行时崩溃
val LocalData = staticCompositionLocalOf<Data> {
    error("No Data provided")
}

@Composable
fun Child() {
    val data = LocalData.current  // 💥 崩溃!
}

// ✅ 确保有 Provider
@Composable
fun App() {
    CompositionLocalProvider(LocalData provides Data()) {
        Child()
    }
}

2. 过度使用 CompositionLocal

// ❌ 所有数据都用 CompositionLocal
val LocalUserName = compositionLocalOf { "" }
val LocalUserAge = compositionLocalOf { 0 }

// ✅ 组合成一个数据类
@Immutable
data class User(val name: String, val age: Int)
val LocalUser = compositionLocalOf<User?> { null }

总结

CompositionLocal 的核心要点: