在 Android Compose 中,CompositionLocal 是一种局部 Composition 范围内的数据共享机制,用于在不通过函数参数显式传递的情况下,让组件树中的后代组件访问上层提供的数据。它解决了“多层组件传递参数”的痛点(避免“prop drilling”),同时保证了数据的局部性(仅对特定 Composition 子树可见)和重组效率(仅依赖数据的组件会重组)。

一、核心概念

1. 什么是 CompositionLocal?

  • 本质是一个“隐式参数容器”,由上层组件通过 CompositionLocalProvider 提供数据,下层组件通过 LocalXXX.current 访问数据。
  • 数据仅在 CompositionLocalProvider 包裹的子树中可见,不影响外部组件,保证了封装性。
  • CompositionLocal 的数据发生变化时,仅依赖该数据的组件会重组,而非整个子树,效率更高。

2. 与其他状态管理的区别

方案适用场景数据作用域重组效率
CompositionLocal局部组件树共享(如主题、配置)局部(子树)精准(仅依赖组件重组)
remember/State单个组件或父子组件状态组件内部/父子精准
ViewModel跨页面/跨组件树共享(如业务数据)全局/页面级需配合 collectAsState
Ambient(已废弃)旧版局部共享局部(子树)效率较低(已被替代)

注意:CompositionLocal 适用于局部、基础设施类数据(如主题、字体、屏幕尺寸、权限状态等),不建议用于业务数据(优先用 ViewModel + 数据流)。

二、基本使用步骤

1. 定义 CompositionLocal

通过 compositionLocalOfstaticCompositionLocalOf 函数创建,两者的核心区别是数据变化时的重组范围

函数重组行为适用场景
compositionLocalOf { 初始值 }仅依赖该数据的组件重组(精准)数据可能变化的场景(如主题切换)
staticCompositionLocalOf { 初始值 }整个 CompositionLocalProvider 子树重组数据永不变化(如固定配置、常量)
示例:定义主题相关的 CompositionLocal
// 1. 定义数据模型(可选,也可直接用基础类型)
data class AppThemeConfig(
    val primaryColor: Color,
    val textSize: TextUnit,
    val isDarkMode: Boolean
)

// 2. 定义 CompositionLocal(使用 compositionLocalOf,支持数据变化)
val LocalAppTheme = compositionLocalOf {
    // 初始值(仅作为 fallback,实际使用时会被 Provider 覆盖)
    AppThemeConfig(
        primaryColor = Color.Blue,
        textSize = 16.sp,
        isDarkMode = false
    )
}

2. 提供数据(CompositionLocalProvider)

CompositionLocalProvider 包裹组件树,通过 value 参数为 CompositionLocal 赋值,该值仅对其子树生效。

示例:在根组件提供主题数据
@Composable
fun MyApp() {
    // 模拟主题状态(可从 ViewModel 或其他来源获取)
    val isDarkMode = remember { mutableStateOf(false) }
    val themeConfig = AppThemeConfig(
        primaryColor = if (isDarkMode.value) Color.DarkGray else Color.Blue,
        textSize = 18.sp,
        isDarkMode = isDarkMode.value
    )

    // 提供 LocalAppTheme 的数据,子树均可访问
    CompositionLocalProvider(LocalAppTheme provides themeConfig) {
        // 子组件(如主页、设置页)
        HomeScreen(
            onToggleDarkMode = { isDarkMode.value = !isDarkMode.value }
        )
    }
}

3. 访问数据(LocalXXX.current)

后代组件通过 LocalXXX.current 直接获取数据,无需通过函数参数传递。

示例:子组件访问主题数据
@Composable
fun HomeScreen(onToggleDarkMode: () -> Unit) {
    // 访问 LocalAppTheme 提供的数据
    val theme = LocalAppTheme.current

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        // 使用主题颜色和文字大小
        Text(
            text = if (theme.isDarkMode) "深色模式" else "浅色模式",
            color = theme.primaryColor,
            fontSize = theme.textSize,
            fontWeight = FontWeight.Bold
        )

        Button(
            onClick = onToggleDarkMode,
            colors = ButtonDefaults.buttonColors(theme.primaryColor)
        ) {
            Text("切换主题")
        }
    }
}

测试代码:

data class ThemeConfig(
    val primaryColor: Color,
    val textSize: TextUnit,
    val isDarkModel: Boolean
)

val CompositionTracerhemeConfig = compositionLocalOf {
    ThemeConfig(Color.Blue,16.sp,false)
}

@Composable
fun test(){
    var isDark = remember { mutableStateOf(false) }
    val themeConfig = ThemeConfig(if(isDark.value) Color.Red else Color.Blue,20.sp,isDark.value)

    CompositionLocalProvider(CompositionTracerhemeConfig provides themeConfig) {
        val theme = CompositionTracerhemeConfig.current
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
            // 使用主题颜色和文字大小
            Text(
                text = if (theme.isDarkModel) "深色模式" else "浅色模式",
                color = theme.primaryColor,
                fontSize = theme.textSize,
                fontWeight = FontWeight.Bold
            )

            Button(
                onClick = {isDark.value = !isDark.value},
                colors = ButtonDefaults.buttonColors(theme.primaryColor)
            ) {
                Text("切换主题")
            }
        }
    }
}

效果:
请添加图片描述

三、高级用法

1. 多层 CompositionLocalProvider(覆盖数据)

可以嵌套 CompositionLocalProvider 覆盖上层提供的数据,子树会优先使用最近一层的赋值。

@Composable
fun NestedDemo() {
    // 外层提供默认主题
    CompositionLocalProvider(LocalAppTheme provides defaultTheme) {
        Text("外层:${LocalAppTheme.current.primaryColor}") // 蓝色

        // 内层覆盖主题
        CompositionLocalProvider(LocalAppTheme provides darkTheme) {
            Text("内层:${LocalAppTheme.current.primaryColor}") // 深灰色
        }
    }
}

2. 结合 remember 存储状态

如果 CompositionLocal 的数据依赖状态(如用户配置),需用 rememberrememberSaveable 存储,避免重组时丢失。

@Composable
fun UserConfigDemo() {
    // 存储用户字体大小配置(旋转屏幕不丢失)
    val userTextSize = rememberSaveable { mutableStateOf(16.sp) }

    val themeConfig = AppThemeConfig(
        primaryColor = Color.Green,
        textSize = userTextSize.value,
        isDarkMode = false
    )

    CompositionLocalProvider(LocalAppTheme provides themeConfig) {
        Column {
            Text("字体大小:${userTextSize.value.value}sp")
            Button(onClick = { userTextSize.value += 2.sp }) {
                Text("增大字体")
            }
        }
    }
}

3. 多个 CompositionLocal 同时提供

CompositionLocalProvider 支持通过 vararg 同时提供多个 CompositionLocal 的数据,简化嵌套。

// 定义另一个 CompositionLocal(如屏幕尺寸)
val LocalScreenSize = compositionLocalOf { WindowSize(0, 0) }

@Composable
fun MultiProviderDemo(windowSize: WindowSize) {
    CompositionLocalProvider(
        LocalAppTheme provides themeConfig,
        LocalScreenSize provides windowSize // 同时提供多个数据
    ) {
        // 子组件可访问 LocalAppTheme 和 LocalScreenSize
        DetailScreen()
    }
}

@Composable
fun DetailScreen() {
    val theme = LocalAppTheme.current
    val screenSize = LocalScreenSize.current
    Text("屏幕宽度:${screenSize.width},主题色:${theme.primaryColor}")
}

四、注意事项

1. 避免滥用

  • CompositionLocal 是“隐式依赖”,过度使用会导致组件依赖不透明(难以追踪数据来源)。
  • 业务数据(如用户信息、列表数据)优先用 ViewModel + Flow/StateFlow,仅基础设施数据(主题、配置)用 CompositionLocal

2. 初始值的意义

compositionLocalOf { 初始值 } 中的初始值是“ fallback ”,仅当组件未被 CompositionLocalProvider 包裹时使用。建议初始值合理(如默认主题),避免空指针。

3. 性能考量

  • compositionLocalOf 而非 staticCompositionLocalOf 处理可变数据,避免不必要的重组。
  • 避免在 CompositionLocal 中存储大量数据或频繁变化的数据(如每秒更新的计数器),可能导致频繁重组。

4. 与 Ambient 的区别

Ambient 是 Compose 1.0 之前的旧 API,已被 CompositionLocal 替代。Ambient 的重组效率较低(修改时整个子树重组),而 compositionLocalOf 仅重组依赖组件,性能更优。

五、常见应用场景

  1. 主题配置:颜色、字体、间距等(如 Compose 内置的 LocalColorsLocalTypography)。
  2. 屏幕尺寸/设备信息:屏幕宽高、是否横屏、设备密度等。
  3. 用户配置:语言、字体大小、深色模式开关等。
  4. 权限状态:是否授予相机/定位权限,供子组件判断是否显示功能。
  5. 导航相关:返回回调、导航控制器(局部范围内)。

总结

CompositionLocal 是 Compose 中高效的局部数据共享方案,核心优势是“隐式传递、精准重组、局部可见”。使用时需遵循“基础设施数据用,业务数据不用”的原则,结合 compositionLocalOfCompositionLocalProvider 实现组件树的优雅数据共享,避免 prop drilling 问题。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐