一、先搞懂:为什么需要 CompositionLocal?
先看一个痛点场景:如果你的界面层级是 AppScreen → Toolbar → TitleText,需要给 TitleText 传递 "主题颜色",不用 CompositionLocal 时,必须层层传参:
kotlin
// 无 CompositionLocal:层层传参,冗余且易出错
@Composable
fun AppScreen() {
val themeColor = Color.Blue // 根组件定义状态
Toolbar(themeColor = themeColor) // 第一层传递
}
@Composable
fun Toolbar(themeColor: Color) {
TitleText(themeColor = themeColor) // 第二层传递
}
@Composable
fun TitleText(themeColor: Color) {
Text("标题", color = themeColor) // 最终使用
}
而用 CompositionLocal 后,无需手动传参,子组件可直接 "获取" 上层定义的状态,这就是它的核心价值:跨层级隐式传递数据,且仅在局部作用域生效。
二、核心概念
1. CompositionLocal 是什么?
-
本质:一个「局部上下文容器」,用于在 Composable 树中传递数据;
-
核心特性:
- 「隐式传递」:子组件无需显式接收参数,直接通过
current获取; - 「作用域隔离」:数据仅在
CompositionLocalProvider包裹的范围内生效; - 「重组隔离」:仅依赖该
CompositionLocal的组件会重组,不影响其他组件; - 「默认值」:定义时可指定默认值,避免空指针。
- 「隐式传递」:子组件无需显式接收参数,直接通过
2. 核心 API 组成
| API | 作用 |
|---|---|
compositionLocalOf<T> { defaultValue } |
创建「不可变」的 CompositionLocal(推荐,性能更高) |
staticCompositionLocalOf<T> { defaultValue } |
创建「静态」的 CompositionLocal(数据极少变化时用) |
CompositionLocalProvider |
为子组件树提供 CompositionLocal 的具体值 |
LocalXXX.current |
子组件获取 CompositionLocal 的当前值 |
三、完整使用步骤(从定义到使用)
核心痛点:没有 CompositionLocal 会多麻烦?
先看「不用共享插座」的场景(对应代码里的 "层层传参"):假设你是一家奶茶店老板,要给店员发「今日特价奶茶」的通知:
- 老板告诉店长:"今日特价是珍珠奶茶";
- 店长告诉前台:"今日特价是珍珠奶茶";
- 前台告诉制作员:"今日特价是珍珠奶茶";
- 制作员告诉打包员:"今日特价是珍珠奶茶"。
只要层级多一层,就要多传一次,漏传 / 传错都容易出问题 ------ 这就是代码里的「Prop Drilling(属性钻取)」,也是 CompositionLocal 要解决的核心问题。
用 CompositionLocal 解决:一步到位
老板直接在店里贴一张「今日特价公告」(定义 + 提供 CompositionLocal),不管是前台、制作员、打包员(子组件),想看直接看公告就行,不用层层传话。
对应到代码:超简单示例
咱们用「奶茶店特价通知」这个场景写代码,全程大白话解释:
步骤 1:定义 "公告板"(创建 CompositionLocal)
先做一个专门贴「今日特价」的公告板,规定好贴的内容类型(比如只能贴奶茶名字),还得有个 "默认值"(没人贴的时候,默认写 "原味奶茶")。
arduino
// 定义:创建一个「今日特价奶茶」的公告板(CompositionLocal)
// 类型是 String,默认值是"原味奶茶"
val LocalSpecialMilkTea = compositionLocalOf<String> {
"原味奶茶" // 没人贴通知时,默认看这个
}
步骤 2:老板贴通知(提供值:CompositionLocalProvider)
老板在店里(父组件)把「今日特价 = 珍珠奶茶」贴到公告板上,并且规定:只要在这个店里的员工(子组件),都能看这个公告。
scss
// 父组件 = 奶茶店
@Composable
fun MilkTeaShop() {
// 老板贴通知:用 CompositionLocalProvider 把公告板和"珍珠奶茶"绑定
CompositionLocalProvider(
LocalSpecialMilkTea provides "珍珠奶茶" // 公告板上写"今日特价:珍珠奶茶"
) {
Column {
// 店里的员工,都能直接看公告板,不用老板挨个说
FrontDesk() // 前台
Maker() // 制作员
Packer() // 打包员
}
}
}
步骤 3:员工看公告(使用值:LocalXXX.current)
前台、制作员、打包员不用老板 / 上一级传话,直接看公告板就行:
kotlin
// 前台(子组件1)
@Composable
fun FrontDesk() {
// 直接看公告板:LocalSpecialMilkTea.current 就是公告内容
val special = LocalSpecialMilkTea.current
Text("前台对顾客说:今日特价是 $special,只要8元!")
}
// 制作员(子组件2)
@Composable
fun Maker() {
val special = LocalSpecialMilkTea.current
Text("制作员:收到!优先做 $special")
}
// 打包员(子组件3)
@Composable
fun Packer() {
val special = LocalSpecialMilkTea.current
Text("打包员:$special 打包好了,贴特价标签!")
}
运行结果:
plaintext
前台对顾客说:今日特价是 珍珠奶茶,只要8元!
制作员:收到!优先做 珍珠奶茶
打包员:珍珠奶茶 打包好了,贴特价标签!
进阶场景:局部改公告(嵌套覆盖)
老板突然说:"下午奶茶店二楼搞活动,特价换成芋泥奶茶"------ 对应代码里的「嵌套 CompositionLocalProvider」:
scss
@Composable
fun MilkTeaShop() {
CompositionLocalProvider(LocalSpecialMilkTea provides "珍珠奶茶") {
FrontDesk() // 一楼前台:看"珍珠奶茶"
Maker() // 一楼制作员:看"珍珠奶茶"
// 二楼单独贴公告:覆盖成"芋泥奶茶"
CompositionLocalProvider(LocalSpecialMilkTea provides "芋泥奶茶") {
Packer() // 二楼打包员:看"芋泥奶茶"
}
}
}
运行结果:
前台对顾客说:今日特价是 珍珠奶茶,只要8元!
制作员:收到!优先做 珍珠奶茶
打包员:芋泥奶茶 打包好了,贴特价标签!
再讲:动态改公告(结合 State)
老板下午改特价了:"今日特价换成草莓奶茶"------ 对应代码里的「动态更新值」:
scss
@Composable
fun MilkTeaShop() {
// 可修改的公告内容(对应Compose的State)
var specialTea by remember { mutableStateOf("珍珠奶茶") }
// 老板改公告:点按钮切换特价
Column {
Button(onClick = { specialTea = "草莓奶茶" }) {
Text("老板:切换今日特价")
}
// 公告板同步更新
CompositionLocalProvider(LocalSpecialMilkTea provides specialTea) {
FrontDesk()
Maker()
}
}
}
点击按钮后,前台和制作员看到的公告会自动变成 "草莓奶茶"------ 这就是 CompositionLocal 结合状态的动态更新。
总结:CompositionLocal 核心 3 点(大白话)
- 定义:做一个 "公告板",规定好能贴啥内容(比如只能贴奶茶名),给个默认值;
- 提供值:老板把 "今日特价" 贴到公告板上,划定 "哪些员工能看"(CompositionLocalProvider 包裹范围);
- 使用值:员工直接看公告板,不用层层传话(LocalXXX.current 获取值)。
什么时候用?什么时候不用?
✅ 用的场景:
- 多个子组件需要用同一个 "配置 / 信息",且层级多(比如 APP 的主题色、字体大小、全局上下文);
- 不想层层传参,想简化代码。
❌ 不用的场景:
- 只有 1-2 层组件(比如老板直接告诉前台),直接传参更简单;
- 全局共享的业务数据(比如用户登录信息),用 ViewModel 更合适(公告板只适合 "局部范围",ViewModel 是 "全店通用的大喇叭")。
简单记:CompositionLocal 是「局部范围的共享公告板」,ViewModel 是「全店通用的大喇叭」,前者解决 "局部层层传参",后者解决 "全局数据共享"。
| 方式 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| 直接传参 | 1-2 层简单传递 | 直观、无额外开销 | 层级多时代码冗余(Prop Drilling) |
| CompositionLocal | 跨层级局部配置传递 | 避免层层传参、作用域隔离、重组精准 | 隐式传递,调试时不易追踪数据来源 |
| ViewModel | 页面级 / 全局业务状态 | 生命周期长、跨组件共享、逻辑与 UI 分离 | 全局作用域,不适合局部配置 |