你什么时候需要 Modifier.composed { ... }?

When do you need Modifier.composed { ... }?

Modifier.composed { ... }什么时候有用?如果我可以简单地在 Modifier.composed { PaddingModifier(...) } 上执行 Modifier.padding(),为什么我需要它?

Modifier.composed allows the creation of a composition-aware modifier-factory that's useful for materializing 实例特定,有状态 修饰符。来自文档:

Declare a just-in-time composition of a Modifier that will be composed for each element it modifies. 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.

换句话说,它允许您将提升状态注入特定于元素的 Modifier 并使用 rememberDisposableEffectAmbient 等。例如:

fun Modifier.fancyModifier(
    enabled: Boolean = false,
    onClick: () -> Unit = {}
) = composed(inspectorInfo = debugInspectorInfo {
    name = "fancyModifier"
    value = enabled
}) {
    var paddingValue by remember { mutableStateOf(0.dp) }
    onCommit(enabled) {
        paddingValue = if (enabled) 16.dp else 0.dp
    }
    fillMaxWidth()
        .clickable { onClick() }
        .padding(paddingValue)
}
LazyColumnFor(items = List(size = 10) { "$it" }) {
    var enabled by remember { mutableStateOf(false) }
    Text(
        text = "fancy modifier",
        modifier = Modifier.fancyModifier(enabled) {
            enabled = !enabled
        }
    )
}

您还可以通过使用 debugInspectorInfo 声明 InspectorInfo 来帮助调试。来自文档:

If inspectorInfo is specified this modifier will be visible to tools during development. Specify the name and arguments of the original modifier. as well as optionally declare InspectorInfo to help with debugging.

如果您想要跟踪的值超过了值,则可以使用 properties 字段而不是 value

注意:debugInspectorInfo lambda 已从 release 构建中删除。

class FancyModifierTest {

    @Before
    fun setup() {
        isDebugInspectorInfoEnabled = true
    }

    @After
    fun teardown() {
        isDebugInspectorInfoEnabled = false
    }

    @Test
    fun testFancyModifierInspectableValue() {
        val modifier = Modifier.fancyModifier() as InspectableValue
        assertEquals(modifier.nameFallback, "fancyModifier")
        assertEquals(modifier.valueOverride, false)
        assertEquals(modifier.inspectableElements.toList().size, 0)
    }

}

这里有一些更实际的例子:

您可以使用它存储内存繁重的对象,而不是每次为特定元素调用该修饰符时都实例化。

这个具有组合的颜色会记住带有索引的颜色,因此在每次重新组合后它 returns 最初随机创建的颜色。

// Creates stateful modifier with multiple arguments
fun Modifier.composedBackground(width: Dp, height: Dp, index: Int) = composed(
    // pass inspector information for debug
    inspectorInfo = debugInspectorInfo {
        // name should match the name of the modifier
        name = "myModifier"
        // add name and value of each argument
        properties["width"] = width
        properties["height"] = height
        properties["index"] = index
    },
    // pass your modifier implementation that resolved per modified element

    factory = {

        val density = LocalDensity.current

        val color: Color = remember(index) {
            Color(
                red = Random.nextInt(256),
                green = Random.nextInt(256),
                blue = Random.nextInt(256),
                alpha = 255
            )
        }

        // add your modifier implementation here
        Modifier.drawBehind {

            val widthInPx = with(density) { width.toPx() }
            val heightInPx = with(density) { height.toPx() }

            drawRect(color = color, topLeft = Offset.Zero, size = Size(widthInPx, heightInPx))
        }
    }
)

每次重新组合可组合项时,这个都会创建一种颜色

fun Modifier.nonComposedBackground(width: Dp, height: Dp) = this.then(

    // add your modifier implementation here
    Modifier.drawBehind {

        //  Without remember this color is created every time item using this modifier composed
        val color: Color = Color(
            red = Random.nextInt(256),
            green = Random.nextInt(256),
            blue = Random.nextInt(256),
            alpha = 255
        )

        val widthInPx = width.toPx()
        val heightInPx = height.toPx()

        drawRect(color = color, topLeft = Offset.Zero, size = Size(widthInPx, heightInPx))
    }
)

用法

        var counter by remember { mutableStateOf(0) }

        Button(
            onClick = { counter++ },
            modifier = Modifier.fillMaxWidth()
        ) {
            Text(text = "Increase $counter")
        }

        TutorialText2(text = "Modifier.composed")
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {

            Box(
                modifier = Modifier
                    .composedBackground(150.dp, 20.dp, 0)
                    .width(150.dp)
            ) {
                Text(text = "Recomposed $counter")
            }

            Box(
                modifier = Modifier
                    .composedBackground(150.dp, 20.dp, 1)
                    .width(150.dp)
            ) {
                Text(text = "Recomposed $counter")
            }
        }

        TutorialText2(text = "Modifier that is not composed")
        Row(
            modifier = Modifier.fillMaxWidth(),
            horizontalArrangement = Arrangement.SpaceEvenly
        ) {

            Box(
                modifier = Modifier
                    .nonComposedBackground(150.dp, 20.dp)
                    .width(150.dp)
            ) {
                Text(text = "Recomposed $counter")
            }

            Box(
                modifier = Modifier
                    .nonComposedBackground(150.dp, 20.dp)
                    .width(150.dp)
            ) {
                Text(text = "Recomposed $counter")
            }
        }

结果

同样在实际情况下,我使用它绘制彩色阴影,它依赖于 Paintremember 块中的原生 Paint 对象,具有与 remember 的键相同的状态对象.

fun Modifier.materialShadow(badgeState: BadgeState) = composed(
    inspectorInfo = {
        name = "shadow"
        value = badgeState.shadow
    },
    factory = {
        if (badgeState.shadow != null) {
            val shadow: MaterialShadow = badgeState.shadow!!
            val isCircleShape = badgeState.isCircleShape

            val paint = remember(badgeState) {
                Paint()
            }

            val frameworkPaint = remember(badgeState) {
                paint.asFrameworkPaint()
            }

            drawBehind {
                this.drawIntoCanvas {

                    val color = shadow.color
                    val dx = shadow.offsetX.toPx()
                    val dy = shadow.offsetY.toPx()
                    val radius = shadow.shadowRadius.toPx()


                    val shadowColor = color
                        .copy(alpha = shadow.alpha)
                        .toArgb()
                    val transparent = color
                        .copy(alpha = 0f)
                        .toArgb()

                    frameworkPaint.color = transparent
                    
                    frameworkPaint.setShadowLayer(
                        dx,
                        dy,
                        radius,
                        shadowColor
                    )

                    if (isCircleShape) {
                        it.drawCircle(
                            center = Offset(center.x, center.y),
                            radius = this.size.width / 2f,
                            paint = paint
                        )
                    } else {
                        it.drawRoundRect(
                            left = 0f,
                            top = 0f,
                            right = this.size.width,
                            bottom = this.size.height,
                            radiusX = size.height * badgeState.roundedRadiusPercent / 100,
                            radiusY = size.height * badgeState.roundedRadiusPercent / 100,
                            paint = paint
                        )
                    }

                }

            }
        } else {
            this
        }
    }
)