一文彻底解析 Compose 的穿透刺客 -- CompositionLocal

发布时间:2024年01月15日

Compose 官方说明一直很简洁:CompositionLocal 是通过组合隐式向下传递数据的工具。


两个核心:隐式、向下传递,我们先看一段非常简单的代码:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                val name = "Hi, Compose!"    // name 是局部变量
                ShowMessage(name)
            }
        }
    }
}

@Composable
fun ShowMessage(message: String) {
    Text(message)
}

这段代码中局部变量 name 以参数的方式被暴露出来了,在 Compose 中这叫做 State Hoisting(状态提升)。

现在思考两个问题:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                val name = "Hi, Compose!"
                ShowMessage(name)
            }
         👉🏻 name =     // 思考 1: 这边能不能获取到 name?
        }
    }
}

@Composable
fun ShowMessage(message: String) {
 👉🏻 name =             // 思考 2: 这边能不能获取到 name?
    Text(message)
}

很明显都是不行的!因为 name 这个局部变量的 作用域 是限定在 ComposeBlogTheme {} 范围内的。

现在我们再来改下代码:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                val name = "Hi, Compose!"
                // ShowMessage(name)    // 这段代码我不要了
            }
        }
    }
}

@Composable
fun ShowMessage() {    // 参数我也去掉,不需要外面传参进来
    Text(name)         // 我就直接把外面的 name 写到这里,直接用
}

这样写行吗?肯定是不行的!但我就想这么写,并且希望它可以运行,那么这种行为就意味着 name 这个局部变量要拥有可以穿出它的 作用域,并且穿透进 showMessage 函数的能力!

好消息,这种牛逼的行为就可以通过我们的主角:CompositionLocal 实现,它提供了这种 「穿透能力」


📓CompositionLocal

现在我们就来实现一个 CompostionLocal 功能,让上面的代码生效,总共需要经历三步:

1. 创建 CompositionLocal 实例

首先创建一个具有 「穿透能力」 的变量:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                val name = "Hi, Compose!"
            }
        }
    }
}

@Composable
fun ShowMessage() {
    Text(name)
}

// 1. 通过 compostionLocalOf 创建 CompositionLocal
val LocalName = compositionLocalOf<String> { "Compose" }

注意:在 Compose 中,一般定义这种具有「穿透能力」CompostionLocal,它的名称有个约定俗成的写法:LocalXXX


2. 为 CompositionLocal 提供值

CompostionLocal 实例定义好了,我们要通过 provides 给它绑定一个值。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                val name = "Hi, Compose!"
                // 2. CompositionLocalProvider 可组合项可将值绑定到给定层次结构的 CompositionLocal 实例
                CompositionLocalProvider(LocalName provides name) {
                    ShowMessage()
                }
            }
        }
    }
}

@Composable
fun ShowMessage() {
    Text(name)
}

// 1. 通过 compostionLocalOf 创建 CompositionLocal
val LocalName = compositionLocalOf<String> { "Compose" }

3. 获取 CompositionLocal 数据

前面两步其实就已经创建好了一个包含 数据 + 可穿透CompositionLocal 了,最后一步就是获取到其中的数据。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            ComposeBlogTheme {
                val name = "Hi, Compose!"
                // 2. CompositionLocalProvider 可组合项可将值绑定到给定层次结构的 CompositionLocal 实例
                CompositionLocalProvider(LocalName provides name) {
                    ShowMessage()
                }
            }
        }
    }
}

@Composable
fun ShowMessage() {
    // 3. 获取 name
    Text(LocalName.current)
}

// 1. 通过 compostionLocalOf 创建 CompositionLocal
val LocalName = compositionLocalOf<String> { "Compose" }

这样就可以了,现在程序可以运行正常了。


📓 CompositionLocal 应用场景

1. 提供上下文

LocalContext.current    // 是不是很熟悉?这其实就相当于 getContext()

2. MaterialTheme

其实官方的 MaterialTheme 就用到了 CompositionLocal,我们看源码:

@Composable
fun MaterialTheme(
    colors: Colors = MaterialTheme.colors,
    typography: Typography = MaterialTheme.typography,
    shapes: Shapes = MaterialTheme.shapes,
    content: @Composable () -> Unit
) {
    val rememberedColors = remember { colors.copy() }.apply { updateColorsFrom(colors) }
    val rippleIndication = rememberRipple()
    val selectionColors = rememberTextSelectionColors(rememberedColors)
    // 通过 providers 将 rememberedColors 提供给了 LocalColors
    CompositionLocalProvider(
        LocalColors provides rememberedColors,
        LocalContentAlpha provides ContentAlpha.high,
        LocalIndication provides rippleIndication,
        LocalRippleTheme provides MaterialRippleTheme,
        LocalShapes provides shapes,
        LocalTextSelectionColors provides selectionColors,
        LocalTypography provides typography
    ) {
        ProvideTextStyle(value = typography.body1) {
            PlatformMaterialTheme(content)
        }
    }
}
object MaterialTheme {
    val colors: Colors
        @Composable
        @ReadOnlyComposable
        get() = LocalColors.current

    val typography: Typography
        @Composable
        @ReadOnlyComposable
        get() = LocalTypography.current

    val shapes: Shapes
        @Composable
        @ReadOnlyComposable
        get() = LocalShapes.current
}

📓 两种创建方式

前面的例子我们使用 compositionLocalOf 创建 CompositionLocal,另外官方还提供了 staticCompositionLocalOf

  1. compositionLocalOf:在重组期间更改提供的值只会使读取其 current 值的内容无效。
  2. staticCompositionLocalOf:与 compositionLocalOf 不同,Compose 不会跟踪 staticCompositionLocalOf 的读取。更改该值会导致提供 CompositionLocal 的整个 content lambda 被重组,而不仅仅是在组合中读取 current 值的位置。

😵😵😵,很懵???我们来看下面的代码:

1. compositionLocalOf

class MainActivity : ComponentActivity() {

    private val themeBackground by mutableStateOf(Color.Blue)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                CompositionLocalProvider(LocalBackground provides Color.Red) {
                    TextBackground()
                    CompositionLocalProvider(LocalBackground provides themeBackground) {
                        /**
                         * 2. themeBackground 变化后,仅此范围重组
                         * TextBackground()
                         */
                        CompositionLocalProvider(LocalBackground provides Color.Red) {
                            TextBackground()
                        }
                    }
                    TextBackground()
                }
            }
        }
    }
}

@Composable
fun TextBackground() {
    Surface(color = LocalBackground.current) {
        Text("Hi, Compose")
    }
}

// 1. Compose 会记录跟踪每一个调用 LocalBackground.current 的区域
val LocalBackground = compositionLocalOf { Color.Blue }

2. staticCompositionLocalOf

class MainActivity : ComponentActivity() {

    private val themeBackground by mutableStateOf(Color.Blue)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            ComposeBlogTheme {
                CompositionLocalProvider(LocalBackground provides Color.Red) {
                    TextBackground()
                    CompositionLocalProvider(LocalBackground provides themeBackground) {
                        /**
                         * 2. themeBackground 变化后,内部所有 content 全部重组
                         * TextBackground()
                         *
                         * CompositionLocalProvider(LocalBackground provides Color.Red) {
                         *    TextBackground()
                         * }
                         */
                    }
                    TextBackground()
                }
            }
        }
    }
}

@Composable
fun TextBackground() {
    Surface(color = LocalBackground.current) {
        Text("Hi, Compose")
    }
}

// Compose 不会记录跟踪每一个调用 LocalBackground.current 的区域
val LocalBackground = staticCompositionLocalOf { Color.Blue }

区别搞懂了吧?那么随之而来一个疑问:我们定义 CompositionLocal 该用哪个?


3. 官方建议

如果为 CompositionLocal 提供的值发生更改的可能性微乎其微或永远不会更改,使用 staticCompositionLocalOf 可提高性能。


比如,我们看两个系统里面定义好的 CompositionLocal

// 上下文固定不变,用 staticCompositionLocalOf
val LocalContext = staticCompositionLocalOf<Context> {
    noLocalProvidedFor("LocalContext")
}

// 内部内容颜色常变,用 compositionLocalOf
val LocalContentColor = compositionLocalOf { Color.Black }
文章来源:https://blog.csdn.net/pepsimaxin/article/details/135601388
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。