CompositionLocal 是 Jetpack Compose 中用于沿着组合树隐式向下传递数据的机制。它类似于 React 的 Context,或者一种"组合树级别的依赖注入"。
它允许你在某个父级提供值,然后在任意深度的子级直接读取,而不需要一层层通过参数显式传递。
1. 一句话理解
-
显式传参:父 → 子 → 孙,每一层都要写参数。
-
CompositionLocal:父级提供一次,子孙任意层级直接读取。
kotlin
val LocalUser = compositionLocalOf<User?> { null }
@Composable
fun App() {
val user = User("Alice")
CompositionLocalProvider(LocalUser provides user) {
HomeScreen() // 中间可以有很多层
}
}
@Composable
fun HomeScreen() {
val user = LocalUser.current
Text("Hello, ${user?.name ?: "Guest"}")
}
HomeScreen 不需要接收 user 参数,直接从当前组合环境中读取。
2. 核心 API
创建 CompositionLocal
kotlin
val LocalUser = compositionLocalOf<User?> { null }
或:
kotlin
val LocalSpacing = staticCompositionLocalOf { 8.dp }
-
compositionLocalOf:会追踪读取,值变化时只重组读取者。 -
staticCompositionLocalOf:不追踪读取,值变化时整个 provider 内容重组。
提供值
kotlin
CompositionLocalProvider(LocalUser provides user) {
Content()
}
可以同时提供多个:
kotlin
CompositionLocalProvider(
LocalUser provides user,
LocalSpacing provides 16.dp
) {
Content()
}
读取值
kotlin
val user = LocalUser.current
.current 只能在 @Composable 函数中读取。
3. 工作原理
CompositionLocal 不是全局变量,它的值按组合树的位置解析。
每个 CompositionLocalProvider 会把 key-value 放入当前组合节点的 CompositionLocalMap。子级读取 .current 时,会从当前节点向上查找最近的 provider,找不到就使用默认值。
所以:
-
同一 CompositionLocal 在不同分支可以有不同的值。
-
子 provider 可以覆盖父 provider。
-
值不会向上或横向传播,只向下。
kotlin
CompositionLocalProvider(LocalUser provides userA) {
HomeScreen() // 读到 userA
CompositionLocalProvider(LocalUser provides userB) {
ProfileScreen() // 读到 userB
}
}
4. 读取追踪与重组
compositionLocalOf 创建的 local,在读取 .current 时会记录当前的重组作用域。当提供的值变化时,只有读取过它的作用域会重组。
kotlin
val LocalColor = compositionLocalOf { Color.Black }
@Composable
fun App() {
var color by remember { mutableStateOf(Color.Red) }
CompositionLocalProvider(LocalColor provides color) {
Text("Hello") // 没读 LocalColor,不重组
ColoredBox() // 读了 LocalColor,会重组
}
}
@Composable
fun ColoredBox() {
val color = LocalColor.current
Box(Modifier.background(color))
}
color 变化时,只有 ColoredBox 重组,Text 不受影响。
如果是 staticCompositionLocalOf,值变化时整个 CompositionLocalProvider 的 content 都会重组,因为它不追踪谁读了。
5. compositionLocalOf vs staticCompositionLocalOf
| 特性 | compositionLocalOf |
staticCompositionLocalOf |
|---|---|---|
| 读取追踪 | 有 | 无 |
| 值变化时 | 只重组读取者 | 整个 provider content 重组 |
| 读取开销 | 稍高 | 更低 |
| 更新开销 | 更低 | 更高 |
| 适用场景 | 可能变化的值 | 几乎不变的值 |
| 例子 | 用户信息、动态主题 | 密度、布局方向、应用主题 |
选择原则:
-
值很少变 →
staticCompositionLocalOf -
值可能变,且只想局部重组 →
compositionLocalOf -
值频繁变 → 考虑提供
State<T>,而不是直接提供T
6. 提供 State<T> 的高级用法
如果值变化频繁,直接提供值会导致读取者重组。可以改为提供 State<T>:
kotlin
val LocalCounter = compositionLocalOf<State<Int>> {
error("No counter provided")
}
@Composable
fun App() {
val count = remember { mutableStateOf(0) }
CompositionLocalProvider(LocalCounter provides count) {
CounterText()
}
}
@Composable
fun CounterText() {
val count = LocalCounter.current.value
Text("Count: $count")
}
CompositionLocalProvider 提供的 State 对象本身不变,所以 provider 不会因为值变化而重组。只有读取 .value 的作用域会通过 Snapshot 系统订阅变化,实现更细粒度的重组。
7. 常见内置 CompositionLocal
Compose 和 Material 已经提供了一些:
-
LocalContext -
LocalConfiguration -
LocalDensity -
LocalLayoutDirection -
LocalContentColor -
LocalTextStyle -
LocalFocusManager -
LocalHapticFeedback -
LocalSoftwareKeyboardController -
LocalUriHandler -
LocalLifecycleOwner -
LocalViewModelStoreOwner -
LocalOnBackPressedDispatcherOwner
例如:
kotlin
val context = LocalContext.current
val density = LocalDensity.current
val viewModelStoreOwner = LocalViewModelStoreOwner.current
MaterialTheme 内部就是用 CompositionLocalProvider 提供颜色、排版、形状等。
LocalContext
-
作用 :提供当前 Android
Context对象。 -
用途 :访问
resources、启动Activity、显示Toast、获取系统服务等。 -
注意 :只能在
@Composable函数中读取.current,不能在onClick等非组合上下文中直接使用。
kotlin
val context = LocalContext.current
Toast.makeText(context, "Hello", Toast.LENGTH_SHORT).show()
LocalConfiguration
-
作用 :提供当前设备的
Configuration对象。 -
用途:获取屏幕尺寸、语言区域、字体缩放等配置信息。当配置变化时,读取该值的 Composable 会自动重组。
-
典型场景:根据系统语言格式化日期、响应屏幕尺寸变化。
kotlin
val configuration = LocalConfiguration.current
val locale = ConfigurationCompat.getLocales(configuration)[0]
LocalDensity
-
作用 :提供当前屏幕密度,用于
dp/sp与像素之间的转换。 -
用途 :在自定义布局或绘制中,将
dp值转换为像素值。
kotlin
val density = LocalDensity.current
val px = with(density) { 16.dp.toPx() }
LocalLayoutDirection
-
作用:提供当前布局方向(LTR 或 RTL)。
-
用途:在自定义布局中,根据语言方向决定子组件的排列顺序。
-
注意 :使用
placeRelative()而非place()可以让子组件自动适应布局方向。
LocalLifecycleOwner
-
作用 :提供当前组合树根部的
LifecycleOwner。 -
用途 :观察 Activity / Fragment 的生命周期状态,配合
LifecycleEventEffect等 API 使用。
kotlin
val lifecycleOwner = LocalLifecycleOwner.current
val state by lifecycleOwner.lifecycle.currentStateFlow.collectAsState()
LocalViewModelStoreOwner
-
作用 :提供当前作用域下的
ViewModelStoreOwner。 -
用途 :
viewModel()函数内部就是通过它来获取 ViewModel 实例的,确保 ViewModel 绑定到正确的生命周期范围(如 Activity、Fragment 或 Navigation 的返回栈条目)。
LocalOnBackPressedDispatcherOwner
-
作用 :提供
OnBackPressedDispatcher,用于处理系统返回按钮事件。 -
用途 :
BackHandler可组合函数底层依赖它来注册返回回调。
kotlin
BackHandler(enabled = true) {
// 处理返回逻辑
}
🎨 主题与样式
LocalContentColor
-
作用:提供当前背景色下推荐的"内容颜色"(用于文字、图标等),确保内容与背景有足够对比度。
-
用途 :
Text、Icon等组件默认使用它来决定颜色。Surface组件会自动设置合适的LocalContentColor。 -
最佳实践 :设置背景色时优先用
Surface而非Modifier.background(),因为Surface会自动更新内容颜色。
LocalTextStyle
-
作用 :提供当前作用域下
Text组件默认使用的TextStyle。 -
用途 :通过
ProvideTextStyle可以为子组件树统一设置文字样式,Text组件默认使用它。
kotlin
ProvideTextStyle(MaterialTheme.typography.bodyLarge) {
Text("这段文字会使用 bodyLarge 样式")
}
⌨️ 输入与交互
LocalFocusManager
-
作用:提供焦点管理器,用于控制焦点移动和清除焦点。
-
用途:在键盘的"下一步"动作中移动焦点,或在"完成"动作中清除焦点以关闭键盘。
kotlin
val focusManager = LocalFocusManager.current
focusManager.moveFocus(FocusDirection.Down) // 移动到下一个
focusManager.clearFocus() // 清除焦点
LocalHapticFeedback
-
作用:提供触觉反馈服务,用于触发振动效果。
-
用途:在点击、长按等交互中提供触觉反馈,增强用户体验。
kotlin
val haptic = LocalHapticFeedback.current
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
LocalSoftwareKeyboardController
-
作用:提供软键盘控制器,用于显示或隐藏软键盘。
-
用途:在用户完成输入后主动隐藏键盘。
kotlin
val keyboardController = LocalSoftwareKeyboardController.current
keyboardController?.hide()
LocalUriHandler
-
作用:提供 URI 处理服务,用于打开网页链接或其他 URI。
-
用途:在点击按钮或文本链接时打开外部浏览器。
kotlin
val uriHandler = LocalUriHandler.current
uriHandler.openUri("https://developer.android.com")
8. 使用场景
适合:
-
主题、颜色、排版、形状
-
密度、布局方向
-
Context、LifecycleOwner、ViewModelStoreOwner
-
跨多层级的通用工具或环境
-
测试、预览时替换依赖
不适合:
-
普通业务数据传递
-
所有参数都用 CompositionLocal 代替
-
频繁变化且影响范围大的状态
原则:显式参数优先,CompositionLocal 用于真正的"环境"或"跨层级上下文"。
9. 最佳实践
-
命名以
Local开头,例如LocalUser。 -
为每个 local 提供合理默认值。
-
值几乎不变 → 用
staticCompositionLocalOf。 -
值可能变 → 用
compositionLocalOf。 -
读取位置尽量下沉到小的 Composable,控制重组范围。
-
不要用 CompositionLocal 传递所有参数。
-
不要在非
@Composable中读取.current。 -
如果值频繁变化,考虑提供
State<T>而不是T。 -
测试和预览时可以用
CompositionLocalProvider注入模拟值。
10. 常见陷阱
在非组合上下文中读取
kotlin
val color = LocalContentColor.current // 必须在 @Composable 中
不能在 onClick、LaunchedEffect、remember 的 calculation 中直接读取:
kotlin
// 错误
val color = remember { LocalContentColor.current }
// 正确
val color = LocalContentColor.current
val remembered = remember(color) { ... }
滥用导致隐式依赖
CompositionLocal 是隐式的,读代码时不容易看出依赖来源。业务数据尽量显式传参,CompositionLocal 只用于环境类数据。
staticCompositionLocalOf 用于频繁变化的值
会导致整个 provider 内容重组,性能很差。频繁变化的值应该用 compositionLocalOf,并延迟读取。
11. 总结
-
CompositionLocal是组合树中隐式向下传值的机制。 -
通过
CompositionLocalProvider提供,通过.current读取。 -
值按树位置解析,子级可覆盖父级,不向上或横向传播。
-
compositionLocalOf追踪读取,只重组读取者。 -
staticCompositionLocalOf不追踪读取,值变化时整个 provider 内容重组。 -
适合主题、环境、上下文、跨层级依赖。
-
不适合普通业务数据传递。
-
读取位置决定重组范围,延迟读取是优化关键。
-
值频繁变化时,可提供
State<T>进一步细粒度控制。
一句话:CompositionLocal 是 Compose 的"隐式环境传递"机制,用得好能简化跨层级依赖,用不好会造成隐式耦合和性能问题。