修饰符工厂函数不应标记为@Composable

Modifier factory functions should not be marked as @Composable

我已经为 Modifier 创建了一个扩展函数,它会在单击时向用户显示 BalloonPopup。我为此功能使用了@Composable 标记,因为我将在气球内显示的内容也是@Composable。但是编译器给我下面的警告;

"修饰符工厂函数不应标记为@Composable,而应使用 composed 代替"

我也应用了建议的更改,但是当用户单击视图时气球根本不显示。

我的问题是;

  1. 为什么修饰符工厂函数不应标记为@Composable
  2. 对于这样的扩展函数,使用@Composable 和组合{ ... } 有什么区别。因为目前,我还没有看到使用@Composable 标签有任何缺点
  3. 为什么不显示气球,即使我在调试代码时代码也通过了 if (showTooltip) 条件。

以下是我使用的函数的代码,包括应用建议前后的代码;

之前:

@Composable
fun Modifier.setPopup(enabled: Boolean = false, content: @Composable BoxScope.() -> Unit): Modifier {
    if (enabled) {
        var anchorOffset by remember { mutableStateOf<LayoutCoordinates?>(null) }
        var showTooltip by remember { mutableStateOf(false) }
        if (showTooltip) {
            BalloonPopup(
                onDismissRequest = {
                    showTooltip = false
                },
                content = content,
                anchorCoordinates = anchorOffset
            )
        }
        return this.clickable {
            showTooltip = true
        }.onGloballyPositioned {
            anchorOffset = it
        }
    } else {
        return this
    }
}

之后:

fun Modifier.setPopup(enabled: Boolean = false, content: @Composable BoxScope.() -> Unit): Modifier = composed {
    if (enabled) {
        var anchorOffset by remember { mutableStateOf<LayoutCoordinates?>(null) }
        var showTooltip by remember { mutableStateOf(false) }
        if (showTooltip) {
            BalloonPopup(
                onDismissRequest = {
                    showTooltip = false
                },
                content = content,
                anchorCoordinates = anchorOffset
            )
        }
        this.clickable {
            showTooltip = true
        }.onGloballyPositioned {
            anchorOffset = it
        }
    } else {
        this
    }
}

这就是我调用扩展函数的方式;

Image(
   modifier = Modifier
                .setPopup(enabled = true) {
                    Text(
                         modifier = Modifier.padding(4.dp),
                         text = "-30 rssi",
                         fontSize = 13.sp
                    )
                },
   painter = painterResource(id = android.R.drawable.ic_secure),
   contentDescription = "Signal Strength"
)

这是在扩展函数中使用的 BalloonPopup class;

suspend fun initTimer(time: Long, onEnd: () -> Unit) {
    delay(timeMillis = time)
    onEnd()
}

@Composable
fun BalloonPopup(
    cornerRadius: Float = 8f,
    arrowSize: Float = 32f,
    dismissTime: Long = 3,
    onDismissRequest: (() -> Unit)? = null,
    anchorCoordinates: LayoutCoordinates? = null,
    content: @Composable BoxScope.() -> Unit
) {
    if (anchorCoordinates != null) {
        var arrowPosition by remember { mutableStateOf(BalloonShape.ArrowPosition.TOP_RIGHT) }

        /**
         * copied from AlignmentOffsetPositionProvider of android sdk and added the calculation for
         * arrowPosition
         * */
        class BalloonPopupPositionProvider(
            val alignment: Alignment,
            val offset: IntOffset
        ) : PopupPositionProvider {
            override fun calculatePosition(
                anchorBounds: IntRect,
                windowSize: IntSize,
                layoutDirection: LayoutDirection,
                popupContentSize: IntSize
            ): IntOffset {
                // TODO: Decide which is the best way to round to result without reimplementing Alignment.align
                var popupPosition = IntOffset(0, 0)

                // Get the aligned point inside the parent
                val parentAlignmentPoint = alignment.align(
                    IntSize.Zero,
                    IntSize(anchorBounds.width, anchorBounds.height),
                    layoutDirection
                )
                // Get the aligned point inside the child
                val relativePopupPos = alignment.align(
                    IntSize.Zero,
                    IntSize(popupContentSize.width, popupContentSize.height),
                    layoutDirection
                )

                // Add the position of the parent
                popupPosition += IntOffset(anchorBounds.left, anchorBounds.top)

                // Add the distance between the parent's top left corner and the alignment point
                popupPosition += parentAlignmentPoint

                // Subtract the distance between the children's top left corner and the alignment point
                popupPosition -= IntOffset(relativePopupPos.x, relativePopupPos.y)

                // Add the user offset
                val resolvedOffset = IntOffset(
                    offset.x * (if (layoutDirection == LayoutDirection.Ltr) 1 else -1),
                    offset.y
                )
                popupPosition += resolvedOffset

                arrowPosition =
                    if (anchorBounds.left > popupPosition.x) {
                        BalloonShape.ArrowPosition.TOP_RIGHT
                    } else {
                        BalloonShape.ArrowPosition.TOP_LEFT
                    }

                return popupPosition
            }
        }

        var isVisible by remember { mutableStateOf(false) }

        AnimatedVisibility(visible = isVisible) {
            Popup(
                popupPositionProvider = BalloonPopupPositionProvider(
                    alignment = Alignment.TopCenter,
                    offset = IntOffset(
                        x = anchorCoordinates.positionInParent().x.roundToInt(),
                        y = anchorCoordinates.positionInParent().y.roundToInt() + anchorCoordinates.size.height
                    )
                ),
                onDismissRequest = onDismissRequest
            ) {
                Box(
                    modifier = Modifier
                        .wrapContentSize()
                        .shadow(
                            dimensionResource(id = R.dimen.cardview_default_elevation),
                            shape = BalloonShape(
                                cornerRadius = cornerRadius,
                                arrowSize = arrowSize,
                                arrowPosition = arrowPosition,
                                anchorCoordinates = anchorCoordinates
                            )
                        )
                        .border(
                            Dp.Hairline, CCTechAppDefaultTheme.primary,
                            shape = BalloonShape(
                                cornerRadius = cornerRadius,
                                arrowSize = arrowSize,
                                arrowPosition = arrowPosition,
                                anchorCoordinates = anchorCoordinates
                            )
                        )
                        .background(
                            shape = BalloonShape(
                                cornerRadius = cornerRadius,
                                arrowSize = arrowSize,
                                arrowPosition = arrowPosition,
                                anchorCoordinates = anchorCoordinates
                            ),
                            color = CCTechAppDefaultTheme.surface
                        )
                        .padding(top = arrowSize.toDP())
                ) {
                    content()
                }
            }
        }

        val coroutineScope = rememberCoroutineScope()
        LaunchedEffect(key1 = Unit, block = {
            isVisible = true
            coroutineScope.launch {
                initTimer(dismissTime * 1000) {
                    isVisible = false
                    (onDismissRequest ?: {}).invoke()
                }
            }
        })

    }
}

class BalloonShape(
    private val cornerRadius: Float,
    private val arrowSize: Float,
    var arrowPosition: ArrowPosition = ArrowPosition.TOP_RIGHT,
    private val anchorCoordinates: LayoutCoordinates
) : Shape {

    enum class ArrowPosition {
        TOP_LEFT, TOP_RIGHT
    }

    override fun createOutline(
        size: Size,
        layoutDirection: LayoutDirection,
        density: Density
    ): Outline {
        val balloonLeft = 0f
        val balloonRight = size.width
        val balloonTop = 0f + arrowSize
        val balloonBottom = size.height

        val arrowTopY = 0f
        val arrowTopX =
            if (arrowPosition == ArrowPosition.TOP_LEFT) balloonLeft + anchorCoordinates.size.width / 2
            else balloonRight - anchorCoordinates.size.width / 2
        val arrowLeftX = arrowTopX - (arrowSize / 2f)
        val arrowLeftY = arrowTopY + arrowSize
        val arrowRightX = arrowTopX + (arrowSize / 2f)
        val arrowRightY = arrowTopY + arrowSize


        val path = Path().apply {
            moveTo(
                x = arrowTopX,
                y = arrowTopY
            )           // Start point on the top of the arrow
            lineTo(
                x = arrowLeftX,
                y = arrowLeftY
            )     // Left edge of the arrow
            lineTo(
                x = balloonLeft,
                y = balloonTop
            )                         // TopLeft edge of the rectangle
            arcTo(
                rect = Rect(
                    left = balloonLeft,
                    top = balloonTop,
                    right = balloonLeft + cornerRadius * 2f,
                    bottom = balloonTop + cornerRadius * 2f
                ),
                startAngleDegrees = 270f,
                sweepAngleDegrees = -90f,
                forceMoveTo = false
            )
            lineTo(
                x = balloonLeft,
                y = balloonBottom
            )                     // Left edge of the rectangle
            arcTo(
                rect = Rect(
                    left = balloonLeft,
                    top = balloonBottom - cornerRadius * 2f,
                    right = balloonLeft + cornerRadius * 2f,
                    bottom = balloonBottom
                ),
                startAngleDegrees = 180f,
                sweepAngleDegrees = -90f,
                forceMoveTo = false
            )
            lineTo(
                x = balloonRight,
                y = balloonBottom
            )                 // Bottom edge of the rectangle
            arcTo(
                rect = Rect(
                    left = balloonRight - cornerRadius * 2f,
                    top = balloonBottom - cornerRadius * 2f,
                    right = balloonRight,
                    bottom = balloonBottom
                ),
                startAngleDegrees = 90f,
                sweepAngleDegrees = -90f,
                forceMoveTo = false
            )
            lineTo(
                x = balloonRight,
                y = balloonTop
            )                     // Right edge of the rectangle
            arcTo(
                rect = Rect(
                    left = balloonRight - cornerRadius * 2f,
                    top = balloonTop,
                    right = balloonRight,
                    bottom = balloonTop + cornerRadius * 2f
                ),
                startAngleDegrees = 0f,
                sweepAngleDegrees = -90f,
                forceMoveTo = false
            )
            lineTo(
                x = arrowRightX,
                y = arrowRightY
            )     //  TopRight edge of the rectangle
            close()
        }
        return Outline.Generic(path)
    }
}

提前致谢。

根据 Compose 修饰符 guidelines:

As a result, Jetpack Compose framework development and Library development SHOULD use Modifier.composed {} to implement composition-aware modifiers, and SHOULD NOT declare modifier extension factory functions as @Composable functions themselves.

为什么

Composed modifiers may be created outside of composition, shared across elements, and declared as top-level constants, making them more flexible than modifiers that can only be created via a @Composable function call, and easier to avoid accidentally sharing state across elements.

在修饰符中使用视图有点老套。修饰符应更改应用它的视图的状态,并且不应影响环境。

您的代码适用于 @Composable,因为它是立即调用的,弹出窗口被添加到视图树中,就好像它是在调用视图之前添加的一样。

使用 composed,稍后会调用内容,当它在渲染之前开始测量视图的位置时 - 由于此代码不是视图树的一部分,因此不会将您的弹出窗口添加到其中。

使用 composed 代码,以便您可以使用 remember 保存状态,还可以使用 LocalDensity 等本地值,但不能用于添加视图。

你几乎可以在你的代码库中做任何你想做的事,毕竟,你可以消除这个警告,但大多数看到你代码的人不会期望这样的修饰符在视图树中添加一个视图——这就是指导方针

我认为实现这种功能的预期方式如下(不确定命名,通过):

@Composable
fun BalloonPopupRequester(
    requesterView: @Composable (Modifier) -> Unit,
    popupContent: @Composable BoxScope.() -> Unit
) {
    var anchorOffset by remember { mutableStateOf<LayoutCoordinates?>(null) }
    var showTooltip by remember { mutableStateOf(false) }
    if (showTooltip) {
        BalloonPopup(
            onDismissRequest = {
                showTooltip = false
            },
            content = popupContent,
            anchorCoordinates = anchorOffset
        )
    }
    requesterView(
        Modifier
            .clickable {
                println("clickable")
                showTooltip = true
            }
            .onGloballyPositioned {
                anchorOffset = it
            }
    )
}

用法:

BalloonPopupRequester(
    requesterView = { modifier ->
        Image(
            modifier = modifier,
            painter = painterResource(id = android.R.drawable.ic_secure),
            contentDescription = "Signal Strength"
        )
    },
    popupContent = {
        Text(
            modifier = Modifier.padding(4.dp),
            text = "-30 rssi",
            fontSize = 13.sp
        )
    }
)