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))
相关推荐
我命由我1234518 小时前
Android 对话框 - 对话框全屏显示(设置 Window 属性、使用自定义样式、继承 DialogFragment 实现、继承 Dialog 实现)
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
Jeled21 小时前
Android 本地存储方案深度解析:SharedPreferences、DataStore、MMKV 全面对比
android·前端·缓存·kotlin·android studio·android jetpack
我命由我123451 天前
Android 开发问题:getLeft、getRight、getTop、getBottom 方法返回的值都为 0
android·java·java-ee·android studio·android jetpack·android-studio·android runtime
alexhilton6 天前
Kotlin互斥锁(Mutex):协程的线程安全守护神
android·kotlin·android jetpack
是六一啊i7 天前
Compose 在Row、Column上使用focusRestorer修饰符失效原因
android jetpack
用户060905255229 天前
Compose 主题 MaterialTheme
android jetpack
用户060905255229 天前
Compose 简介和基础使用
android jetpack
用户060905255229 天前
Compose 重组优化
android jetpack
行墨9 天前
Jetpack Compose 深入浅出(一)——预览 @Preview
android jetpack