Home > Blockchain >  Android JetPack Compose - Understanding @Composable scopes
Android JetPack Compose - Understanding @Composable scopes

Time:01-05

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

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.

But I call two composable functions from the setContent block. It has no problems calling MyLayout(), so why does it have problems with calling PlaceMarkerOnImage(Offset) ?

CodePudding user response:

PlaceMarkerOnImage is not called from setContent, it is called from inside of MyLayout. If you want to pass composable function as an argument to another function, you have to annotate the argument with @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.

What you have to do is something like this:

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

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

CodePudding user response:

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. 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.

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

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. That is not how Compose works as Compose is a declarative framework, not an imperative framework.

In a declarative framework, the UI is built with transforms that transform data into user interface. Whenever data observed by the transforms changes, the transforms are re-run, and any changes in the result are reflected in the 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.

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. When the transform changes its mind about what the UI is, the UI changes to that. 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. The UI is controlled by and can only be changed by the invocation of a composable function.

The transform function is (mostly) executed synchronously and the result of the invocation is the UI. In the above, you call placeMarker in a callback function after composition has completed. 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. Calling it in isolation is much like just adding two numbers together, like a b, but not storing the result anywhere. 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.

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. 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. Compose doesn't actually run all composable functions (for performance reasons) but is a good mental model to have.

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. Also, since composable functions are nouns, not verbs, this should just be called marker. This would mean that the function would be something like,

@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,

    ...
    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.

  • Related