Android JetPack Compose - 了解 @Composable 范围

Fel*_*lix 4 android kotlin android-jetpack-compose composable

我现在对此有点抓狂,无论我看了多少教程和读了多少代码片段,我根本无法理解这个概念。

我只是想将一个标记图像放在我点击的另一个图像的顶部。

class MainActivity : ComponentActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    setContent {

        MyLayout() {
            PlaceMarkerOnImage(it)
        }
    }
}

@Composable
private fun MyLayout(
    placeMarker: (Offset) -> Unit
) {
    val painter: Painter = painterResource(id = R.drawable.image)

    Column(Modifier.fillMaxSize()) {

        Box(
            modifier = Modifier.weight(0.95f)
        ) {
            Image(
                contentScale = FillBounds,
                painter = painter,
                contentDescription = "",
                modifier = Modifier.pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            placeMarker(it)
                        }
                    )
                }
            )
        }
        Button(
            onClick = { },
            modifier = Modifier.weight(0.05f),
            shape = MaterialTheme.shapes.small
        ) {
            Text(text = "Edit Mode")
        }

    }
}

@Composable
private fun PlaceMarkerOnImage(offset: Offset) {
    Image(
        painter = painterResource(id = R.drawable.marker),
        contentScale = ContentScale.Crop,
        contentDescription = "",
        modifier = Modifier.offset(offset.x.dp, offset.y.dp)
    )
}
}
Run Code Online (Sandbox Code Playgroud)

但这是错误的,因为我在调用时遇到了可怕的编译错误PlaceMarkerOnImage@Composable 调用只能从 @Composable 函数的上下文中发生

我不明白..我得到的是重写的onCreate函数不是 @Composable,因此不能从中调用 @Composable 函数,也不能只向它添加 @Composable 注释。

但我从块中调用了两个可组合函数setContent。通话没有问题MyLayout(),那么为什么通话会出现问题呢PlaceMarkerOnImage(Offset)

chu*_*ckj 8

PlaceMarkerOnImage从机械上讲,您无法在传递给的 lambda 内部调用的原因MyLayout是因为它没有被标记,@Composable因此 lambda 不被认为是可组合的。然而,这只是把罐子踢了几英尺,因为一旦你做出改变,编译器就会抱怨对placeMarkerin的调用onTap

这里的脱节是你在使用声明性框架时强制地思考。

在命令式框架中,UI 的状态是通过创建 UI 树来构建的,然后解释该树以在屏幕上生成 UI(传统上使用布局和绘制或类似的命名阶段)。每次更改树时,都会重复布局和绘制步骤(通常在下一个并条机上)。要更改 UI,您可以创建新的树元素并将它们放置在正确的位置,或者更改树中已有元素的属性。

上面的内容似乎建议您将可组合函数视为生成新内容,并且在调用时,将在调用它们的任何外部可组合函数中生成该内容。这不是 Compose 的工作方式,因为 Compose 是一个声明式框架,而不是一个命令式框架。

在声明式框架中,UI 是通过将数据转换为用户界面的转换来构建的。每当转换观察到的数据发生更改时,就会重新运行转换,并且结果中的任何更改都会反映在 UI 中。

在声明性框架中,转换描述了应该向 UI 提供哪些数据,并且添加、删除或更改 UI 的唯一方法是通过更改转换观察到的数据来修改转换生成的内容。

换句话说,命令式框架中的事物是用动词(创建、修改、删除)来描述的。一个声明性框架,它是一个名词。也就是说,它描述了 UI 是什么,而不是如何创建它。当转换改变关于 UI 的想法时,UI 也会随之改变。无需描述如何到达那里,这是框架的工作。

变换的编码方式、产生的内容、检测变化的方式以及重新执行变换的时间和方式在每个声明性框架中都各不相同。

在 Compose 中,转换是函数,观察到的数据作为参数传递给这些函数。UI 由可组合函数控制,并且只能通过调用可组合函数来更改。

转换函数(大部分)是同步执行的,调用的结果是 UI。在上面,您在placeMarker合成完成后调用回调函数。由于它没有作为组合的一部分调用,因此编译器将其标记为错误。可组合函数只能从另一个可组合函数调用,因为结果必须是组合的一部分。单独调用它就像将两个数字加在一起,例如a + b,但不将结果存储在任何地方。当您调用可组合函数时,您会说“此函数的内容放在这里”,这仅在从另一个可组合函数调用时才有意义。因此,编译器会检查并报告何时在无意义的上下文中调用可组合函数。

请记住,可组合函数可以任意多次运行,并且应该始终从相同的数据产生相同的结果。值得一提的是,随着数据的每次更改,所有可组合函数都会重新运行,并且运行后生成的 UI 就是您看到的 UI。Compose 实际上并不运行所有可组合函数(出于性能原因),但它是一个很好的思维模型。

对上面的最简单的更改onTap是修改一些正在观察的数据MyLayout,然后根据该数据调用placeMarker或不调用。另外,由于可组合函数是名词,而不是动词,因此应该将其称为marker。这意味着该函数将类似于,

@Composable
private fun MyLayout(
    marker: @Composable (Offset) -> Unit
) {
    val painter: Painter = painterResource(id = R.drawable.image)

    Column(Modifier.fillMaxSize()) {

        Box(
            modifier = Modifier.weight(0.95f)
        ) {
            var showMarker by remember { mutableStateOf(false) }
            var markerOffset by remember { mutableStateOf(Offset.Zero) }
            Image(
                contentScale = FillBounds,
                painter = painter,
                contentDescription = "",
                modifier = Modifier.pointerInput(Unit) {
                    detectTapGestures(
                        onTap = {
                            showMarker = true
                            markerOffset = it
                        }
                    )
                }
            )
            if (showMarker) {
                marker(markerOffset)
            }
        }
        Button(
            onClick = { },
            modifier = Modifier.weight(0.05f),
            shape = MaterialTheme.shapes.small
        ) {
            Text(text = "Edit Mode")
        }

    }
}
Run Code Online (Sandbox Code Playgroud)

一旦熟悉了这个模型,添加事件处理程序来删除标记就相当简单了,例如,

    ...
    onDoubleTap = { showMarker = false },
    ...
Run Code Online (Sandbox Code Playgroud)

在命令式框架中,这是一件众所周知的棘手事情,因为当标记尚未显示时收到双击时,或者当您较晚收到事件时,在树的这一部分被删除后,您需要处理,等等。所有这些问题都由 Compose 运行时为您处理。