简体   繁体   English

Android JetPack Compose - 了解@Composable 作用域

[英]Android JetPack Compose - Understanding @Composable scopes

I'm kinda pulling my hair out about this for a while now, I simply can't grasp the concept no matter how many tutorials i watch and code snippets I read..一段时间以来,我一直在为这个问题烦恼,无论我看了多少教程和阅读了多少代码片段,我都无法理解这个概念。

I simply want to put a marker image on top of another image where i tap it.我只是想将标记图像放在我点击它的另一个图像之上。

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)
    )
}
}

But this is wrong since I get the dreaded compilation error upon calling PlaceMarkerOnImage : @Composable invocations can only happen from the context of a @Composable function但这是错误的,因为我在调用PlaceMarkerOnImage时遇到了可怕的编译错误: @Composable 调用只能发生在 @Composable function 的上下文中

I don't get it.. what I got is that the overridden onCreate function is not @Composable, hence no @Composable functions can be called from it nor can i just add the @Composable annotation to it.我不明白.. 我得到的是被覆盖的onCreate function 不是 @Composable,因此不能从它调用 @Composable 函数,我也不能只向它添加 @Composable 注释。

But I call two composable functions from the setContent block.但是我从setContent块中调用了两个可组合函数。 It has no problems calling MyLayout() , so why does it have problems with calling PlaceMarkerOnImage(Offset) ?调用MyLayout()没有问题,那么为什么调用PlaceMarkerOnImage(Offset)有问题?

Mechanically the reason you cannot call PlaceMarkerOnImage inside the lambda passed to MyLayout is because it isn't marked @Composable therefore the lambda is not considered composable.从机械上讲,您无法在传递给MyLayout的 lambda 中调用PlaceMarkerOnImage的原因是因为它未标记为@Composable ,因此 lambda 不被视为可组合。 That, however, just kicks the can down the road a few feet as, as soon as you make that change, the compiler will complain about the call to placeMarker in onTap .然而,这只是将罐头踢了几英尺,因为一旦您进行了更改,编译器就会抱怨在onTap中调用placeMarker

The disconnect here is you are thinking imperatively when using a declarative framework.这里的脱节是您在使用声明性框架时正在思考。

In an imperative framework, the state of the UI is built up by creating the UI tree and then the tree is interpreted to produce the UI on the screen (traditionally with layout and draw or similar named phases).在命令式框架中,UI 的 state 是通过创建 UI 树构建的,然后解释树以在屏幕上生成 UI(传统上使用布局和绘制或类似的命名阶段)。 Every time the tree is changed the layout and draw steps are repeated (usually on the next draw frame).每次更改树时,都会重复布局和绘制步骤(通常在下一个绘制框架上)。 To change the UI you create new tree elements and place them in the right location or change the properties of the elements already in the tree.要更改 UI,您可以创建新的树元素并将它们放置在正确的位置,或者更改树中已有元素的属性。

The above seems to suggest you view composable functions as generating new content and, when called, will generate that content in whatever outer composable function they are called from.上面的内容似乎建议您将可组合函数视为生成新内容,并且在调用时将在调用它们的任何外部可组合 function 中生成该内容。 That is not how Compose works as Compose is a declarative framework, not an imperative framework.这不是 Compose 的工作方式,因为 Compose 是一个声明式框架,而不是命令式框架。

In a declarative framework, the UI is built with transforms that transform data into user interface.在声明性框架中,UI 是使用将数据转换为用户界面的转换构建的。 Whenever data observed by the transforms changes, the transforms are re-run, and any changes in the result are reflected in the UI.每当转换观察到的数据发生变化时,转换都会重新运行,结果中的任何更改都会反映在 UI 中。

In a declarative framework the transforms describes what the UI should be given some data and the only way to add, remove, or change the UI is to modify what is produced by the transforms by changing the data observed by the transform.在声明性框架中,转换描述了应该为 UI 提供哪些数据,添加、删除或更改 UI 的唯一方法是通过更改转换观察到的数据来修改转换产生的内容。

In other words, a imperative framework things are described in terms of verbs (create, modify, remove).换句话说,命令式框架中的事物是用动词(创建、修改、删除)来描述的。 A declarative framework, it is a noun.一个声明性的框架,它是一个名词。 That is, It describes what the UI is, not how to create it.也就是说,它描述了 UI 是什么,而不是如何创建它。 When the transform changes its mind about what the UI is, the UI changes to that.当转换改变它对 UI 是什么的想法时,UI 也会随之改变。 No need to describe how to get there, that is the job of the framework.无需描述如何到达那里,那是框架的工作。

How the transform is encoded, what it produces, how changes are detected, and when and how the transform is re-executed all vary in each declarative framework.转换的编码方式、它产生的内容、更改的检测方式以及转换重新执行的时间和方式在每个声明性框架中都各不相同。

In Compose, the transforms are functions and the data observed is passed as parameters to these functions.在 Compose 中,转换是函数,观察到的数据作为参数传递给这些函数。 The UI is controlled by and can only be changed by the invocation of a composable function. UI 由可组合的 function 的调用控制并且只能通过调用来更改。

The transform function is (mostly) executed synchronously and the result of the invocation is the UI.转换 function(大部分)是同步执行的,调用的结果是 UI。 In the above, you call placeMarker in a callback function after composition has completed.在上面,您在合成完成后在回调 function 中调用placeMarker As it is not called as part of a composition, this is marked by the compiler as an error.由于它不作为组合的一部分调用,因此编译器将其标记为错误。 A composable function can only be called from another composable function as the result must be part of a composition.可组合项 function 只能从另一个可组合项 function 调用,因为结果必须是组合的一部分。 Calling it in isolation is much like just adding two numbers together, like a + b , but not storing the result anywhere.单独调用它很像将两个数字加在一起,如a + b ,但不将结果存储在任何地方。 When you call a composable function you are saying, "the content of this function goes here" which is only meaningful when called from another composable function. The compiler, therefore, checks that and reports when a composable function is called in a context that does not make sense.当您调用可组合项 function 时,您是在说“此 function 的内容位于此处”,这仅在从另一个可组合项 function 调用时才有意义。因此,编译器会检查并报告何时在执行以下操作的上下文中调用可组合项 function没有意义。

Keep in mind that a composable function can be run arbitrarily and many times and should always produce the same result from the same data.请记住,可组合的 function 可以任意多次运行,并且应该总是从相同的数据中产生相同的结果。 It is helpful to think that, with every change in data, all composable functions are re-run and, after running, the UI produced is the UI you see.考虑一下,随着数据的每次更改,所有可组合的功能都会重新运行,运行后生成的 UI 就是您看到的 UI,这很有帮助。 Compose doesn't actually run all composable functions (for performance reasons) but is a good mental model to have. Compose 实际上并不运行所有可组合函数(出于性能原因),但它是一个很好的心理 model 拥有。

The simplest change to what you have above to change onTap to modify some data that MyLayout is observing and then, based on that data, call placeMarker or not.对上面所做的最简单的更改是更改onTap以修改MyLayout正在观察的一些数据,然后根据该数据调用placeMarker或不调用。 Also, since composable functions are nouns, not verbs, this should just be called marker .此外,由于可组合函数是名词,而不是动词,因此它应该被称为marker This would mean that the function would be something like,这意味着 function 类似于,

@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")
        }

    }
}

Once you are familiar with this model then adding an event handler to remove the marker is rather straight-forward, such as,一旦您熟悉了这个 model ,那么添加一个事件处理程序来删除标记就相当简单了,例如,

    ...
    onDoubleTap = { showMarker = false },
    ...

which is a notoriously tricky thing to do in an imperative framework as you need to handle when you receive a double tap when the marker is not showing already or when you receive the event late, after the tree this part of the tree has been removed, etc. All these issues are handled by the Compose runtime for you.在命令式框架中,这是众所周知的棘手事情,因为当标记尚未显示时收到双击或收到事件晚时,您需要处理,在树之后,树的这一部分已被删除,等等。所有这些问题都由 Compose 运行时为您处理。

PlaceMarkerOnImage is not called from setContent , it is called from inside of MyLayout . PlaceMarkerOnImage不是从setContent调用的,它是从MyLayout内部调用的。 If you want to pass composable function as an argument to another function, you have to annotate the argument with @Composable :如果您想将可组合 function 作为参数传递给另一个 function,则必须使用@Composable注释该参数:

@Composable
private fun MyLayout(
    placeMarker: @Composable (Offset) -> Unit
)

This won't solve your problem though, it will just move it to onTap , because onTap argument of detectTapGestures doesn't accept composable function either.但这不会解决您的问题,它只会将其移至onTap ,因为detectTapGesturesonTap参数也不接受可组合的 function 。

What you have to do is something like this:你要做的是这样的:

setContent {
    var markerOffset by remember { mutableStateOf<Offset?>(null) }

    MyLayout { markerOffset = it }
    
    markerOffset?.let {
        PlaceMarkerOnImage(it)
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM