相关文章:
Compose编程思想 -- Compose中重组风险和优化
前言
在之前的文章中,我介绍了CompositionLocal的使用,它具备@Composable
函数的穿透作用,在CompositionLocalProvider
提供的作用域内,任意@Composable
函数都可以使用其提供的局部变量,具有移除函数参数的作用,但是只会在上下文、主题等场景下使用,如果有不熟悉的伙伴,可以查看文章开头的相关文章。
那么本节我将会介绍另外一种提供局部变量的方式,即ModifierLocal
。
1 ModifierLocal的使用
例如在Column中第一个子组件中,连续调用了两次layout
,假设在第一个layout
中创建一个成员变量firstWidth
,想要在第二个layout
中使用,其实也是一种穿透的效果。
kotlin
@Composable
fun TestModifierLocal() {
Column {
Text(text = "第一个元素", modifier = Modifier
.layout { measurable, constraints ->
val size = measurable.measure(constraints)
// 这是一个成员变量
val firstWidth = size.width
layout(size.width, size.height) {
size.placeRelative(0, 0)
}
}
.layout { measurable, constraints ->
val size = measurable.measure(constraints)
layout(size.width, size.height) {
size.placeRelative(0, 0)
}
})
Text(text = "第二个元素")
}
}
1.1 ModifierLocalProvider介绍
在使用ModifierLocal
的时候,也有一个用于提供共享参数的Provider -- ModifierLocalProvider
。
kotlin
/**
* A Modifier that can be used to provide [ModifierLocal]s that can be read by other modifiers to
* the right of this modifier, or modifiers that are children of the layout node that this
* modifier is attached to.
*/
@Stable
@JvmDefaultWithCompatibility
interface ModifierLocalProvider<T> : Modifier.Element {
/**
* Each [ModifierLocalProvider] stores a [ModifierLocal] instance that can be used as a key
* by a [ModifierLocalConsumer] to read the provided value.
*/
val key: ProvidableModifierLocal<T>
/**
* The provided value, that can be read by modifiers on the right of this modifier, and
* modifiers added to children of the composable using this modifier.
*/
val value: T
}
用于提供ModifierLocal,可以被当前Modifier右侧的Modifiers读到,即上游定义的ModifierLocal,在下游可以被读到。
ModifierLocalProvider
也是一个Modifier.Element
,它提供了key-value键值对,通过key可以取到定义的value,这个key其实就是ModifierLocal
。
kotlin
Modifier.modifierLocalProvider(modifierLocalOf { "" }) { "共享数据" }
通过Modifier的modifierLocalProvider
可以提供一个ModifierLocal
用于给Modifier下游的Modifier提供局部变量的读取操作。
kotlin
/**
* A Modifier that can be used to provide [ModifierLocal]s that can be read by other modifiers to
* the right of this modifier, or modifiers that are children of the layout node that this
* modifier is attached to.
*/
@ExperimentalComposeUiApi
fun <T> Modifier.modifierLocalProvider(key: ProvidableModifierLocal<T>, value: () -> T): Modifier {
return this.then(
object : ModifierLocalProvider<T>,
InspectorValueInfo(
debugInspectorInfo {
name = "modifierLocalProvider"
properties["key"] = key
properties["value"] = value
}
) {
override val key: ProvidableModifierLocal<T> = key
override val value by derivedStateOf(value)
}
)
}
那么既然有存操作,就会有取操作,取值的操作就是通过ModifierLocalConsumer
,它是用来消费左侧的Modifier设置的ModifierLocal
。
kotlin
/**
* A Modifier that can be used to consume [ModifierLocal]s that were provided by other modifiers to
* the left of this modifier, or above this modifier in the layout tree.
*/
@Stable
@JvmDefaultWithCompatibility
interface ModifierLocalConsumer : Modifier.Element {
/**
* This function is called whenever one of the consumed values has changed.
* This could be called in response to the modifier being added, removed or re-ordered.
*/
fun onModifierLocalsUpdated(scope: ModifierLocalReadScope)
}
1.2 ModifierLocal使用
前面我们知道了ModifierLocalProvider
和ModifierLocalConsumer
是用来提供数据和消费数据的,那么如何在上下游将其串联起来。
通过源码我们可以发现,ModifierLocalProvider
和ModifierLocalConsumer
都是Modifier.Element
,因此可以采用CombinedModifier
融合两个Element来达成效果。
kotlin
@Composable
fun TestModifierLocal() {
//需要共享的key
val mKey = modifierLocalOf { "" }
Column {
Text(text = "第一个元素", modifier = Modifier
.then(object : LayoutModifier, ModifierLocalProvider<String> {
//需要共享的value
var mValue: String? = null
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
Log.d("TAG", "measure: left")
val size = measurable.measure(constraints)
// 在测量的时候,赋值
mValue = size.width.toString()
return layout(size.width, size.height) {
size.placeRelative(0, 0)
}
}
override val key: ProvidableModifierLocal<String>
get() = mKey
override val value: String
get() = mValue ?: ""
})
.then(object : LayoutModifier, ModifierLocalConsumer {
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
Log.d("TAG", "measure: right")
val size = measurable.measure(constraints)
return layout(size.width, size.height) {
size.placeRelative(0, 0)
}
}
override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
with(scope) {
Log.d("TAG", "onModifierLocalsUpdated: ${mKey.current}")
}
}
}))
Text(text = "第二个元素")
}
}
在上游通过Combined的方式实现了将LayoutModifier
和ModifierLocalProvider
的融合,注意这里在赋值mValue
是在LayoutNode测量的时候,那么这种情况下,能保证下游取数据的时候,是在测量完成之后吗?
显然不能,除非在业务层做这种保障,像上述的这种方式是无法保证的,具体原因看下1.3小节对于源码的介绍。那么如何保证上游定义的局部变量,可以在下游使用的时候,准确地获取到?
最主要的原因就是,ModifierLocal不是这么用的,因为onModifierLocalsUpdated
的调用是一定要比测量和布局早的,因此在测量过程中对变量操作是无效的,因此只有在onModifierLocalsUpdated
中处理完数据,下游才能拿到处理完的数据,因此需要同时实现ModifierLocalProvider
和ModifierLocalConsumer
接口。
kotlin
@Composable
fun TestModifierLocal() {
//需要共享的key
val mKey = modifierLocalOf { "" }
Column {
Text(
text = "第一个元素", modifier = Modifier
.then(object : LayoutModifier, ModifierLocalProvider<String>,
ModifierLocalConsumer {
//需要共享的value
lateinit var mValue: String
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val size = measurable.measure(constraints)
// 在测量的时候,赋值
return layout(size.width, size.height) {
size.placeRelative(0, 0)
}
}
override val key: ProvidableModifierLocal<String>
get() = mKey
override val value: String
get() = mValue
override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
with(scope) {
val initValue = mKey.current
mValue = initValue + "LEFT"
}
}
})
.then(object : LayoutModifier, ModifierLocalConsumer,
ModifierLocalProvider<String> {
lateinit var mValue: String
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val size = measurable.measure(constraints)
return layout(size.width, size.height) {
size.placeRelative(0, 0)
}
}
override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
with(scope) {
mValue = mKey.current
Log.d("TAG", "onModifierLocalsUpdated: ${mKey.current}")
}
}
override val key: ProvidableModifierLocal<String>
get() = mKey
override val value: String
get() = mValue
})
)
Text(text = "第二个元素")
}
}
在上游的onModifierLocalsUpdated
中对mValue
做初始化的操作,在下游的onModifierLocalsUpdated
中就可以拿到对应的值。
在Compose中,其实提供了一些与ModifierLocal
相关的函数,例如windowInsetsPadding
,就是实现了ModifierLocalProvider
和ModifierLocalConsumer
接口,下面我也是使用了这种方式实现一个可以附加padding的需求。
kotlin
// 共享的padding key
internal val ModifierLocalConsumerPaddingKey = modifierLocalOf { 0.dp }
/**
* 可重复累加的padding
*/
fun Modifier.multiPadding(padding: Dp) =
then(object : LayoutModifier, ModifierLocalProvider<Dp>, ModifierLocalConsumer {
var paddingValue: Dp = 0.dp
override fun MeasureScope.measure(
measurable: Measurable,
constraints: Constraints
): MeasureResult {
val placeable = measurable.measure(constraints)
//加padding
val width = paddingValue.value + placeable.width * 2
val height = paddingValue.value + placeable.height * 2
return layout(width.toInt(), height.toInt()) {
placeable.placeRelative(0, 0)
}
}
override fun onModifierLocalsUpdated(scope: ModifierLocalReadScope) {
with(scope) {
val current = ModifierLocalConsumerPaddingKey.current
// 上下游的padding累加
paddingValue = current + padding
}
}
override val key: ProvidableModifierLocal<Dp>
get() = ModifierLocalConsumerPaddingKey
override val value: Dp
get() = paddingValue
})
其实使用ModifierLocal
可以用来实现Modifier存在上下联动的这种场景中,下游数据会依赖上游数据的时候,可以像Flow这样,在上游将数据处理完成之后,可以在下游拿到处理完成的数据。
1.3 ModifierLocal原理分析
在Modifier初始化的时候,如果按照1.2中我自定义的Modifier,那么在Modifier.Element
转Modifier.Node
的过程中,会创建BackwardsCompatNode
,在调用onAttach
函数时,会执行initializeModifier
函数。
kotlin
// BackwardsCompatNode.kt
override fun onAttach() {
initializeModifier(true)
}
kotlin
private fun initializeModifier(duringAttach: Boolean) {
check(isAttached)
val element = element
if (isKind(Nodes.Locals)) {
if (element is ModifierLocalProvider<*>) {
updateModifierLocalProvider(element)
}
if (element is ModifierLocalConsumer) {
if (duringAttach)
updateModifierLocalConsumer()
else
sideEffect { updateModifierLocalConsumer() }
}
}
// ......
}
在这个函数中,首先会判断当前节点如果是ModifierLocalProvider
,那么会执行updateModifierLocalProvider
函数,这个函数主要是用来将定义的key存储到ModifierLocalManager
中的一个数组中,其实就是将ModifierLocal
存到一个集合里。
kotlin
private fun updateModifierLocalProvider(element: ModifierLocalProvider<*>) {
val providedValues = _providedValues
if (providedValues != null && providedValues.contains(element.key)) {
providedValues.element = element
requireOwner()
.modifierLocalManager
.updatedProvider(this, element.key)
} else {
_providedValues = BackwardsCompatLocalMap(element)
// we only need to notify the modifierLocalManager of an inserted provider
// in the cases where a provider was added to the chain where it was possible
// that consumers below it could need to be invalidated. If this layout node
// is just now being created, then that is impossible. In this case, we can just
// do nothing and wait for the child consumers to read us. We infer this by
// checking to see if the tail node is attached or not. If it is not, then the node
// chain is being attached for the first time.
val isChainUpdate = requireLayoutNode().nodes.tail.isAttached
if (isChainUpdate) {
requireOwner()
.modifierLocalManager
.insertedProvider(this, element.key)
}
}
}
然后,再判断如果当前节点为ModifierLocalConsumer
,那么就会执行其onModifierLocalsUpdated
函数。
kotlin
fun updateModifierLocalConsumer() {
if (isAttached) {
readValues.clear()
requireOwner().snapshotObserver.observeReads(
this,
updateModifierLocalConsumer
) {
(element as ModifierLocalConsumer).onModifierLocalsUpdated(this)
}
}
}
所以从源码中就可以看出,onModifierLocalsUpdated
的执行发生在Modifier初始化的过程中,还没有到measure
和layout
的过程中,所以这也印证了我前面的结论。因此对于Modifier共享的数据,只能在onModifierLocalsUpdated
中完成初始化,在测量绘制的过程中调用,不可做赋值处理。