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
  • 作用:提供当前背景色下推荐的"内容颜色"(用于文字、图标等),确保内容与背景有足够对比度。

  • 用途TextIcon 等组件默认使用它来决定颜色。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 中

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

相关推荐
三少爷的鞋2 小时前
Kotlin 协程闯关:看代码,猜结果
android
2501_932750264 小时前
Android 跑马灯:从一行 XML 到自定义控件
android
BoomHe16 小时前
Android Framework 文件应用移植到 AndroidStudio
android
传奇开心果编程17 小时前
【Jetpack Compose基础语法学与练】第8课 rememberSaveable,页面旋转/系统重建保留状态
android·学习·ui·kotlin·android jetpack
>Andre<17 小时前
UFS5.0标准中文全译·卷一:范围、术语与架构
android·linux·嵌入式硬件
事圆则缓18 小时前
Java 8 Lambda、Stream、Optional 与 Android 边界:从回调语法到运行时兼容
android·java
mmsx18 小时前
Android 地图十万要素不卡顿:空间网格 + 渐进式加载的移动端实践
android·大数据·opengl
Godikov20 小时前
Android 工业终端保活实战:前台服务 + 开机自启 + 更新自启的三重保障
android
字节暗面20 小时前
SO加固强度怎么量化?腾讯ACE与FairGuard静态分析实测
android·逆向