Compose CompositionLocal 详解

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 的"隐式环境传递"机制,用得好能简化跨层级依赖,用不好会造成隐式耦合和性能问题。

相关推荐
mmsx1 小时前
Android 防二次打包第一道锁:签名校验与完整性校验
android·kotlin
mmsx1 小时前
Android 混淆不等于安全:二次打包链路完整走一遍
android·kotlin
小宋10213 小时前
Agent轨迹级评测实战:工具选择、预算超限与回归门禁
android·网络·人工智能·回归
墨天梦5 小时前
D05_ViewModel与单向数据流
android·kotlin
蒸鱼Yuzheng6 小时前
Android 构建可复现性:APK 指纹、文件级差异与供应链审计
android·apk·devops·软件供应链·可复现构建
墨天梦8 小时前
D03_Compose列表与稳定身份
android·gitee·kotlin
萌新杰少9 小时前
Kuikly股票查看软件开发体验——SaiRen
android·kotlin·客户端
vilya9 小时前
把 Python 塞进 APK:Chaquopy 打包实践
android·python
用户92817267390169 小时前
Android Compose版本的AI组件库来了。
android·kotlin
知昂七昂9 小时前
00-02:AOSP 源码阅读环境与检索方法论源码剖析(Android 16 / aosp-main)
android