Modifier.composed() 和 ComposedModifier

ComposedModifier:深入理解与实践指南

ComposedModifier 基础介绍

ComposedModifier是一个特殊的Modifier,它实现了Element接口。

kotlin 复制代码
// ComposedModifier.kt
private open class ComposedModifier(
    inspectorInfo: InspectorInfo.() -> Unit,
    val factory: @Composable Modifier.() -> Modifier // 具有@Composable上下文环境,接收者类型是Modifier,返回一个Modifier
) : Modifier.Element, InspectorValueInfo(inspectorInfo)

ComposedModifier本身不提供任何的实际功能,也就是对界面没有作用。它的作用是将我们想要提供的Modifier包装在一个工厂函数factory里面,在界面组合时,会去执行这个工厂函数,获取实际的Modifier对象。

它的核心特点是在组合过程中动态创建 Modifier,而不是在声明时立即创建。

ComposedModifier 的工作原理

创建与调用流程

使用ComposedModifier特别简单,调用composed()函数即可。

kotlin 复制代码
fun Modifier.composed(
    inspectorInfo: InspectorInfo.() -> Unit = NoInspectorInfo,
    factory: @Composable Modifier.() -> Modifier
): Modifier = this.then(ComposedModifier(inspectorInfo, factory))

像下面这样:

kotlin 复制代码
Box(
    modifier = Modifier.composed { 
        Modifier.padding(8.dp) 
    }
)

那工厂函数factory具体的调用时机是什么?

比如在上面的代码中,会先调用composed函数,创建ComposedModifier实例,但此时工厂函数还没有被调用,当包含了ComposedModifier的组件Box执行时,就会去调用工厂函数,返回实际的Modifier对象。

源码解析

找一下调用的源码,首先进入Box函数:

kotlin 复制代码
// Box.kt
@Composable
fun Box(modifier: Modifier) {
    Layout(measurePolicy = EmptyBoxMeasurePolicy, modifier = modifier) // 进入Layout
}

然后,进入 Layout 函数:

kotlin 复制代码
// Layout.kt
@Composable
@UiComposable
inline fun Layout(
    modifier: Modifier = Modifier,
    measurePolicy: MeasurePolicy
) {
    val compositeKeyHash = currentCompositeKeyHash
    val materialized = currentComposer.materialize(modifier) // 进入materialize
    // ..
}

最后,进到 materialize 函数中:

kotlin 复制代码
// ComoposedModifier.kt
fun Composer.materialize(modifier: Modifier): Modifier {
    if (modifier.all { it !is ComposedModifier }) {
        return modifier
    }

    // ..

    val result = modifier.foldIn<Modifier>(Modifier) { acc, element ->
        acc.then(
            if (element is ComposedModifier) {
                @Suppress("UNCHECKED_CAST")
                val factory = element.factory as Modifier.(Composer, Int) -> Modifier
                val composedMod = factory(Modifier, this, 0)
                materialize(composedMod)
            } else {
                element
            }
        )
    }

    endReplaceableGroup()
    return result
}

就是在 materialize 函数中处理 ComposedModifier的。

materialize 函数中,会先判断当前可组合函数的Modifier链是否包含ComposedModifier,如果不包含直接返回。

包含的话,会遍历Modifier链,遇到ComposedModifier类型的元素,就调用它的factory工厂函数,将得到的结果作为materialize函数的参数,再次调用materialize函数(也就是递归处理结果);遇到非ComposedModifier,就直接添加到结果链中。

简单来说,就是将Modifier链中所有ComposedModifier类型的Modifier,调用它的工厂函数factory,变为非ComposedModifier类型的Modifier。

比如这个Modifier链:

kotlin 复制代码
modifier = Modifier
    .size(100.dp)
    .composed {
        Modifier.padding(8.dp)
    }
    .background(Color.Green)

处理过程:

这下我们知道了ComposedModifier的工厂函数factory,确实会在组合的过程中被执行。

有状态 Modifier 与状态隔离

有状态 Modifier

那么这个composed()函数的作用就只是延迟创建Modifier实例而已吗?

我们先来看看这个函数的注释:

sql 复制代码
Declare a just-in-time composition of a Modifier that will be composed for each element it modifies.
声明一个 Modifier 的即时组合,该组合将为它修饰的每个元素单独执行。
composed may be used to implement stateful modifiers that have instance-specific state for each modified element, allowing the same Modifier instance to be safely reused for multiple elements while maintaining element-specific state.
composed 可用于实现有状态的修饰符,这些修饰符为每个被修饰的元素维护特定的实例状态,从而允许同一个 Modifier 实例安全地被多个元素重用,同时保持元素特定的状态。

关键术语解释:

  • 即时组合(just-in-time composition) :指Modifier不是在声明时立即创建,而是在组合过程中根据需要动态创建
  • 元素特定的实例状态(instance-specific state for each modified element) :每个使用该Modifier的组件都会获得自己独立的状态实例
  • 安全重用(safely reused) :同一个Modifier的定义,可以用在多个不同的组件中,而不会导致状态混淆或冲突

看完后,你可能还是很疑惑,别担心,结合代码理解。

你只是这样写的话:

kotlin 复制代码
// 写法1:composed()
val modifier = Modifier.composed {
    Modifier.padding(8.dp)
}
Box(modifier = modifier)
Text(text = "Hello World!", modifier = modifier)

// 写法2:正常创建Modifier
val modifier1 = Modifier.padding(8.dp)
Box(modifier1)
Text(text = "Hello World!", modifier = modifier1)

那这两种写法的唯一区别就是:Modifier实例的创建时间,对界面显示没有任何影响。

所以它的使用场景不在于这, 而是用于有状态的Modifier的。

那什么是有状态 Modifier?

这里的状态和Composable函数中的状态是一个意思,是指包含内部状态(如 mutableStateOf)的 Modifier。这些状态可以影响 Modifier 的行为和外观。

比如这样写,Modifier就有了状态。

kotlin 复制代码
val modifier = Modifier.composed {
    // 声明状态
    var isActive by remember { mutableStateOf(false) }

    // 基于状态返回不同的修饰符
    Modifier
        .clickable { isActive = !isActive }
        .background(if (isActive) Color.Red else Color.Green)
}

我们可以在这个Modifier里面维护内部状态(管理状态),并且每一个使用了这个Modifier的元素,都有自己独立的状态实例,我们可以将我们的Modifier,用在多个地方。

但是实际开发中,为了代码的可读性、可维护性,不会这样写的,而是这样写:

kotlin 复制代码
// 声明状态
var isActive by remember { mutableStateOf(false) }

// 基于状态返回不同的修饰符
val modifier = Modifier
    .clickable { isActive = !isActive }
    .background(if (isActive) Color.Red else Color.Green)

所以这种有状态的Modifier 的使用场景,也不在这,而是在于自定义Modifier

比如这样:

kotlin 复制代码
// 创建自定义Modifier
@SuppressLint("ModifierFactoryUnreferencedReceiver")
fun Modifier.myStatefulModifier(): Modifier = composed {
    // 声明状态
    var isActive by remember { mutableStateOf(false) }

    // 基于状态返回不同的修饰符
    Modifier
        .clickable { isActive = !isActive }
        .background(if (isActive) Color.Red else Color.Green)
}

// 使用自定义Modifier
Box(Modifier
    .size(100.dp)
    .myStatefulModifier())

状态隔离

自定义Modifier时,使用 composed 就可以确保状态隔离,如果不使用 composed()(不状态隔离),当多个组件使用同一个 Modifier 定义时,这些组件会共享同一个状态实例,导致意外的情况发生:

kotlin 复制代码
fun Modifier.incorrectStatefulModifier(): Modifier {
    // 状态会被所有使用此修饰符的元素共享
    var isActive by mutableStateOf(false)

    return this
        .clickable { isActive = !isActive }
        .background(if (isActive) Color.Red else Color.Green)
}

如果有多个元素应用了上面的Modifier,点击任何一个元素都会改变所有元素的背景颜色,因为它们共享同一个 isActive 状态。

ComposedModifier 的使用场景

需要状态管理的自定义 Modifier

上面有例子。

读取 CompositionLocal 值

由于工厂函数factory具有 @Composable 上下文,所以可以访问当前组合上下文中的 CompositionLocal 值,比如访问当前主题颜色:

kotlin 复制代码
fun Modifier.themeDependentBackground(): Modifier = composed {
    // 访问当前主题颜色
    val primaryColor = MaterialTheme.colorScheme.primary
    val surfaceColor = MaterialTheme.colorScheme.surface

    // 使用主题颜色创建渐变背景
    this.background(
        brush = Brush.linearGradient(
            colors = listOf(primaryColor, surfaceColor)
        )
    )
}

访问当前的 Context 或其他上下文信息:

kotlin 复制代码
// 获取上下文 
val context = LocalContext.current

调用Composable 函数

composed() 中可以调用其他 Composable 函数(原因同上),如 LaunchedEffectDisposableEffect 等:

点击后旋转的Modifier

我们现在来实现一个自定义的有状态 Modifier,效果:当点击元素后,会开始旋转,再次点击停止旋转。

其实有一个和这个类似的效果,就是掘金个人主页中的头像动画效果:把鼠标悬停在头像上,头像会开始旋转,并且然后越来越快,最后匀速。

效果如下:

我们实现的效果和他是不同的,只是提一下,那开始实现吧,先搞定旋转。我们创建一个无终止的动画,然后设置元素的旋转角度即可。

注意:修改动画时长,即可修改旋转速度。

kotlin 复制代码
fun Modifier.CustomRotate(): Modifier = composed {
    // 创建无限动画
    val infiniteTransition = rememberInfiniteTransition(label = "rotateOnHover")

    // 旋转角度
    val rotation by infiniteTransition.animateFloat(
        initialValue = 0f,
        targetValue = 360f,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 3000, easing = LinearEasing), 
            repeatMode = RepeatMode.Restart
        ),
        label = "rotation"
    )

    // 旋转
    rotate(rotation)
}

效果:

可以看到已经可以旋转了。

再来完成点击后才开始旋转:根据是否在旋转,来设置不同的目标角度。

kotlin 复制代码
@SuppressLint("ModifierFactoryUnreferencedReceiver") // Modifier 扩展函数中不使用 `this` 接收者,就需要加上这个注解
fun Modifier.rotateOnClick(): Modifier = composed {
    // 是否旋转
    var isRotating by remember { mutableStateOf(false) }

    // 创建无限旋转的动画
    val infiniteTransition = rememberInfiniteTransition(label = "rotateOnHover")

    // 初始值和目标值
    val initialValue = 0f
    val targetValue = if (isRotating) 360f else 0f

    // 旋转角度
    val rotation by infiniteTransition.animateFloat(
        initialValue = initialValue,
        targetValue = targetValue,
        animationSpec = infiniteRepeatable(
            animation = tween(durationMillis = 3000, easing = LinearEasing),
            repeatMode = RepeatMode.Restart
        ),
        label = "rotation"
    )


    // 应用点击事件和旋转效果
    clickable {
        isRotating = !isRotating
    }.rotate(rotation)
}


Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
    Box(
        Modifier
            .size(100.dp)
            .rotateOnClick()
            .background(Color.Green)
    )
}

效果:

可以看到点击后,就会开始旋转了。

最佳实践

何时使用 composed()

就是ComposedModifier 的使用场景:

  1. 需要状态管理:当自定义 Modifier 需要维护内部状态时
  2. 需要访问 Composable 上下文:当需要访问 CompositionLocal、Context 或其他上下文信息时
  3. 需要使用其他 Composable 函数:当需要使用 LaunchedEffect、DisposableEffect 等函数时
  4. 需要使用动画 API:当需要使用 animateXxxAsState 或其他动画 API 时

其中第二点,我觉得是最重要的。

避免过度使用

不要在所有自定义 Modifier 中都使用 composed(),只在必要时使用。过度使用会增加组合成本和内存使用。

composed() 不再推荐使用

主要由于性能问题(特别是在频繁重组的情况下),Google 已经不再推荐使用 composed() 方法了。

替代方案有:

  1. 使用带有 @Composable 注解的 Modifier 扩展函数
  2. 使用Modifier.Node API

示例比较: 使用 composed()

kotlin 复制代码
// 使用 composed() 的方式
fun Modifier.fade(enable: Boolean): Modifier = composed {
    val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
    graphicsLayer { 
        this.alpha = alpha 
    }
}

使用带有 @Composable 注解的 Modifier 扩展函数

kotlin 复制代码
@Composable
fun Modifier.fade(enable: Boolean): Modifier {
    val alpha by animateFloatAsState(if (enable) 0.5f else 1.0f)
    return graphicsLayer { 
        this.alpha = alpha 
    }
}

使用Modifier.Node API

kotlin 复制代码
// 创建自定义 Modifier
class FadeModifierNode(
    var enable: Boolean
) : ModifierNodeElement<FadeModifierNode.Companion.Node>() {
    
    override fun create(): Node = Node(enable)
    
    override fun update(node: Node) {
        node.enable = enable
    }
    
    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is FadeModifierNode) return false
        return enable == other.enable
    }
    
    override fun hashCode(): Int = enable.hashCode()
    
    companion object {
        class Node(
            var enable: Boolean
        ) : Modifier.Node(), DrawModifierNode {
            private var alpha = 0f
            private val animatable = Animatable(initialValue = 1f)
            
            override fun onAttach() {
                coroutineScope.launch {
                    animatable.animateTo(if (enable) 0.5f else 1f)
                }
            }
            
            override suspend fun onUpdate() {
                animatable.animateTo(if (enable) 0.5f else 1f)
            }
            
            override fun ContentDrawScope.draw() {
                alpha = animatable.value
                drawContent()
                drawRect(
                    color = Color.Black.copy(alpha = 1f - alpha),
                    blendMode = BlendMode.DstIn
                )
            }
        }
    }
}

// 扩展函数,使用更方便
fun Modifier.fadeNode(enable: Boolean): Modifier = this.then(FadeModifierNode(enable))
相关推荐
我命由我1234512 天前
Android 解绑服务问题:java.lang.IllegalArgumentException: Service not registered
android·java·开发语言·java-ee·安卓·android jetpack·android-studio
我命由我1234513 天前
MQTT - Android MQTT 编码实战(MQTT 客户端创建、MQTT 客户端事件、MQTT 客户端连接配置、MQTT 客户端主题)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
前行的小黑炭14 天前
Android LiveData源码分析:为什么他刷新数据比Handler好,能更节省资源,解决内存泄漏的隐患;
android·kotlin·android jetpack
_一条咸鱼_14 天前
深度剖析:Java PriorityQueue 使用原理大揭秘
android·面试·android jetpack
_一条咸鱼_14 天前
揭秘 Java PriorityBlockingQueue:从源码洞悉其使用原理
android·面试·android jetpack
_一条咸鱼_14 天前
深度揭秘:Java LinkedList 源码级使用原理剖析
android·面试·android jetpack
_一条咸鱼_14 天前
深入剖析 Java LinkedBlockingQueue:源码级别的全面解读
android·面试·android jetpack
_一条咸鱼_14 天前
探秘 Java DelayQueue:源码级剖析其使用原理
android·面试·android jetpack
_一条咸鱼_14 天前
揭秘 Java ArrayDeque:从源码到原理的深度剖析
android·面试·android jetpack
_一条咸鱼_14 天前
深入剖析!Android WebView使用原理全解析:从源码底层到实战应用
android·面试·android jetpack