CompositionLocal
是通过组合隐式向下传递数据的工具
主要学习内容
- 了解什么是
CompositionLocal
- 创建自己的
CompositionLocal
- 何时使用
CompositionLocal
显示传参与隐式传参
显示传参
@Test
fun Surface() {
val textColor = "红色"
Column(textColor)
}
fun Column(textColor: String) {
println("Column color:$textColor")
Button(textColor)
}
fun Button(textColor: String) {
println("Button color:$textColor")
Text(textColor)
}
fun Text(textColor: String) {
println("Text color:$textColor")
}
显示传参中数据以参数的形式向下流经整个调用过程,可以看到为了传递textColor
参数需要每个调用方都增加textColor
参数
隐式传参
var textColor = "红色"
@Test
fun Surface() {
Column()
}
fun Column() {
println("Column color:$textColor")
Button()
}
fun Button() {
println("Button color:$textColor")
Text()
}
fun Text() {
println("Text color:$textColor")
}
隐式传参中数据以共有数据的形式传递,不需要额外参数
注:此时的
Surface
、Column
等只是普通函数
函数内修改值
显示传参
@Test
fun Surface() {
val textColor = "红色"
Column(textColor)
println("----------")
Column(textColor)
}
//未修改Column,省略Column代码
fun Button(textColor: String) {
textColor="黑色"
println("Button color:$textColor")
Text(textColor)
}
//未修改Text,省略Text代码
Column color:红色
Button color:黑色
Text color:黑色
----------
Column color:红色
Button color:黑色
Text color:黑色
显示传参中,修改数据的值不会影响下一次调用,即数据隔离效果。而且可以影响下游的值
隐式传参
var textColor = "红色"
@Test
fun Surface() {
Column()
println("----------")
Column()
}
//未修改Column,省略Column代码
fun Button() {
textColor="黑色"
println("Button using:$textColor")
Text()
}
//未修改Text,省略Text代码
Column color:红色
Button color:黑色
Text color:黑色
----------
Column color:黑色
Button color:黑色
Text color:黑色
第一次调用没有问题,但第二次调用时textColor
就全为黑色,即隐式传参中值的修改不是局部的,会导致其他地方对于textColor
的使用发生变化
那么若我们只想在Button
及其子元素中修改textColor
值,在其他地方仍为原来的值该怎么办呢?
我们可以记录textColor
原本的值,在使用完后再将原本的值重新赋值给textColor
fun Button() {
val origin = textColor
textColor = "黑色"
println("Button color:$textColor")
Text()
textColor = origin
}
在kotlin中我们还可以进一步封装该过程
fun Button() {
provider("黑色") {
Text()
println("Button color:$textColor")
}
}
fun provider(changed: String, action: () -> Unit) {
val origin = textColor
textColor = changed
action()
textColor = origin
}
虽然这个代码比较简易,但是与
CompositionLocal
的原理大同小异。可以基于这个大致理解CompositionLocal
,若想深入理解CompositionLocal
需要去查阅源码
简介
在 Compose 中往往可组合项调用另一个可组合项,若所有数据都以参数形式向下流经整个界面树传递给每个可组合函数,那么对于广泛使用的常用数据(如颜色或类型样式),这可能会很麻烦,无论是在编写代码还是维护代码时
为了支持无需将颜色作为显式参数依赖项传递给大多数可组合项,Compose 提供了 CompositionLocal
,可让您创建以树为作用域的具名对象,这可以用作让数据流经界面树的一种隐式方式
CompositionLocal
元素通常在界面树的某个节点以值的形式提供。该值可供其可组合项的后代使用,而无需在可组合函数中将 CompositionLocal
声明为参数
在MaterialTheme
对象中提供了三个 CompositionLocal
实例,即 colors、typography 和 shapes。我们可以在之后的可组合函数中检索这些实例
具体来说,这些是可以通过
MaterialTheme
colors
、shapes
和typography
属性访问的LocalColors
、LocalShapes
和LocalTypography
属性
@Composable
fun MyApp() {
MaterialTheme {
SomeTextLabel("CompositionLocal")
}
}
@Composable
fun SomeTextLabel(labelText: String) {
Text(
text = labelText,
// 通过LocalColors隐式传值
color = MaterialTheme.colors.primary
)
}
CompositionLocal
实例的作用域限定为组合的一部分,因此您可以在结构树的不同级别提供不同的值。CompositionLocal
的 current
值对应于该组合部分中最接近的祖先提供的值
在Column
中修改了CompositionLocal
的值,于是在组合树中分为了两个作用域范围
ListItem
中获取CompositionLocal
的current
值就对应于Column
提供的值
BottomNavigation
中获取CompositionLocal
的current
值就对应于Scaffold
提供的值,而不是Column
提供的值,因为Column
不是BottomNavigation
的祖先节点而是兄弟节点
如需为 CompositionLocal
提供新值,请使用 CompositionLocalProvider
及其 provides
infix 函数,该函数将 CompositionLocal
键与 value
相关联。在访问 CompositionLocal
的 current
属性时,CompositionLocalProvider
的 content
lambda 将获取提供的值。提供新值后,Compose 会重组读取 CompositionLocal
的组合部分
@Composable
fun CompositionLocalExample() {
//MaterialTheme中LocalContentAlpha值默认为ContentAlpha.high
MaterialTheme {
Column {
Text("Uses MaterialTheme's provided alpha")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
Text("Medium value provided for LocalContentAlpha")
//通过使用其`current`属性获取`CompositionLocal`当前值
Text("This Text also uses the medium value:${LocalContentAlpha.current}")
CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.disabled) {
DescendantExample()
}
}
}
}
}
@Composable
fun DescendantExample() {
// CompositionLocalProvider可以跨函数作用
Text("This Text uses the disabled alpha now")
}
Material 可组合项会在内部使用CompositionLocal
,我们可以通过使用其current
属性获取CompositionLocal
当前值
注意:
CompositionLocal
对象或常量通常带有Local
前缀,以便在 IDE 中利用自动填充功能提高可检测性
自定义CompositionLocal
CompositionLocal
是通过组合隐式向下传递数据的工具
使用 CompositionLocal
的另一个关键点是该参数是横切参数(隐式传参)且中间层的实现不需要知道该参数的存在。例如,对 Android 权限的查询是由 CompositionLocal
在后台提供的。媒体选择器可组合项继续添加新功能,无需根据权限是否获取去修改其API。只需要媒体选择器的调用方知道权限的获取情况
这里吐槽一句,辣鸡谷歌机翻(╯' - ')╯︵ ┻━┻
但是,CompositionLocal
并非始终是最好的解决方案。不建议过度使用 CompositionLocal
,其存在一些缺点:
-
CompositionLocal
使得可组合项的行为更难推断Material 组件中大量使用
CompositionLocal
方式传递值,若不熟悉这些组件则很难判断出组件最后呈现效果和应修改那些CompositionLocal
值 -
在创建隐式依赖项时,使用这些依赖项的可组合项的调用方需要确保为每个
CompositionLocal
提供一个值该依赖项可能没有明确的可信来源,因为它可能会在组合中的任何部分发生改变,会使得测试时难度变高
CompositionLocal
非常适合基础架构,而且 Jetpack Compose 大量使用该工具
使用条件
CompositionLocal
应具有合适的默认值。如果没有默认值,在开发时很容易陷入不提供CompositionLocal
导致的异常
如果创建测试或预览使用该 CompositionLocal
的可组合项时也需要显式提供默认值,那么不提供默认值不仅会带来问题还会造成了糟糕的使用体验
有些概念并非以树或子层次结构为作用域,请避免对这些概念使用 CompositionLocal
,建议使用CompositionLocal
的情况为:其可能会被任何(而非少数几个)后代使用
一种错误做法的示例是创建在特定界面使用的
ViewModel
的CompositionLocal
,以便该屏幕中的所有可组合项都可以获取ViewModel
来执行某些逻辑因为特定界面下并不是所有可组合项都需要知道
ViewModel
。最佳做法是使用状态向下传递而事件向上传递的单向数据流模式,或只向可组合项传递所需信息。这样做会使可组合项的可重用性更高,并且更易于测试
创建CompositionLocal
有两个 API 可用于创建 CompositionLocal
:
-
compositionLocalOf
:如果更改提供的值,会使读取其current
值的组件发生重组 -
staticCompositionLocalOf
:与compositionLocalOf
不同,Compose 不会跟踪staticCompositionLocalOf
的读取。更改该值会导致提供CompositionLocal
的整个content
lambda 被重组,而不仅仅是在组合中读取current
值的位置
如果为 CompositionLocal
提供的值发生更改的可能性微乎其微或永远不会更改,使用 staticCompositionLocalOf
可提高性能
例:
data class Elevations(val card: Dp = 0.dp, val default: Dp = 0.dp)
// 定义一个带有默认值的compostionlocal全局对象
// 这个实例可以被应用中的所有可组合项访问
val LocalElevations = compositionLocalOf { Elevations() }
为CompositionLocal提供值
CompositionLocalProvider
可组合项可将值绑定到给定层次结构的 CompositionLocal
实例。如需为 CompositionLocal
提供新值,请使用 provides
infix 函数,该函数将 CompositionLocal
键与 value
相关联,如下所示:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// 基于系统主题创建不同的Elevations
val elevations = if (isSystemInDarkTheme()) {
Elevations(card = 1.dp, default = 1.dp)
} else {
Elevations(card = 0.dp, default = 0.dp)
}
// 将elevations赋值给LocalElevations
CompositionLocalProvider(LocalElevations provides elevations) {
// ... Content goes here ...
// 可组合项都可以通过LocalElevations.current获取到elevations实例
}
}
}
}
使用CompositionLocalProvider
CompositionLocal.current
返回由最接近的 CompositionLocalProvider
(其向该 CompositionLocal
提供一个值)提供的值:
@Composable
fun SomeComposable() {
Card(elevation = LocalElevations.current.card) {
// Content
}
}
替换方案
在某些情况中,CompositionLocal
可能是一种过度的解决方案。如果您的用例不符合CompositionLocal
使用条件,其他解决方案可能更适合您的用例
传递显示参数
显式使用可组合项的依赖项是一种很好的习惯。建议仅传递所需可组合项。为了鼓励分离和重用可组合项,每个可组合项包含的信息应该可能少
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
// ...
MyDescendant(myViewModel.data)
}
// ✘ 不要传递整个对象! 应该只传递需要的部分
// 同样也不要使用 CompositionLocal 隐式传递 ViewModel
@Composable
fun MyDescendant(myViewModel: MyViewModel) { ... }
// 只传递需要的部分
@Composable
fun MyDescendant(data: DataToDisplay) {
// Display data
}
控制反转
即不是由后代接受依赖项来执行某些逻辑,而是由父级接受依赖项来执行某些逻辑,也就是状态向下传递而事件向上传递的单向数据流模式
在以下示例中,后代需要触发请求以加载某些数据:
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
...
MyDescendant(myViewModel)
}
@Composable
fun MyDescendant(myViewModel: MyViewModel) {
Button(onClick = { myViewModel.loadData() }) {
Text("Load data")
}
}
此时我们可以考虑将MyDescendant
中Button
的点击事件上传,在MyComposable
中执行myViewModel.loadData()
,即事件上传
@Composable
fun MyComposable(myViewModel: MyViewModel = viewModel()) {
...
ReusableLoadDataButton(
onLoadClick = {
myViewModel.loadData()
}
)
}
@Composable
fun ReusableLoadDataButton(onLoadClick: () -> Unit) {
Button(onClick = onLoadClick) {
Text("Load data")
}
}
此方法将子级与其直接祖先实体分离开来将子级与其直接祖先实体分离。虽然祖先实体可组合项往往越来越复杂,这样就可以使更低级别的可组合项更灵活