在 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 | 类型 | 用途 |
|---|---|---|
LocalContext | Context | Android Context |
LocalConfiguration | Configuration | 设备配置 |
LocalDensity | Density | 像素密度,dp/px 转换 |
LocalLayoutDirection | LayoutDirection | 布局方向(LTR/RTL) |
LocalLifecycleOwner | LifecycleOwner | 生命周期所有者 |
LocalContentColor | Color | 当前内容颜色 |
使用内置 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 的核心要点:
- 用途:跨组件树传递共享数据,避免层层传参
- staticCompositionLocalOf:值很少变化,如主题、配置
- compositionLocalOf:值可能变化,需要精确重组控制
- CompositionLocalProvider:提供值,支持嵌套覆盖
- 性能:注意选择合适的类型,避免不必要的重组
- 适度使用:只用于真正需要跨层级共享的数据