簡體   English   中英

Android JetPack Compose - 了解@Composable 作用域

[英]Android JetPack Compose - Understanding @Composable scopes

一段時間以來,我一直在為這個問題煩惱,無論我看了多少教程和閱讀了多少代碼片段,我都無法理解這個概念。

我只是想將標記圖像放在我點擊它的另一個圖像之上。

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

但這是錯誤的,因為我在調用PlaceMarkerOnImage時遇到了可怕的編譯錯誤: @Composable 調用只能發生在 @Composable function 的上下文中

我不明白.. 我得到的是被覆蓋的onCreate function 不是 @Composable,因此不能從它調用 @Composable 函數,我也不能只向它添加 @Composable 注釋。

但是我從setContent塊中調用了兩個可組合函數。 調用MyLayout()沒有問題,那么為什么調用PlaceMarkerOnImage(Offset)有問題?

從機械上講,您無法在傳遞給MyLayout的 lambda 中調用PlaceMarkerOnImage的原因是因為它未標記為@Composable ,因此 lambda 不被視為可組合。 然而,這只是將罐頭踢了幾英尺,因為一旦您進行了更改,編譯器就會抱怨在onTap中調用placeMarker

這里的脫節是您在使用聲明性框架時正在思考。

在命令式框架中,UI 的 state 是通過創建 UI 樹構建的,然后解釋樹以在屏幕上生成 UI(傳統上使用布局和繪制或類似的命名階段)。 每次更改樹時,都會重復布局和繪制步驟(通常在下一個繪制框架上)。 要更改 UI,您可以創建新的樹元素並將它們放置在正確的位置,或者更改樹中已有元素的屬性。

上面的內容似乎建議您將可組合函數視為生成新內容,並且在調用時將在調用它們的任何外部可組合 function 中生成該內容。 這不是 Compose 的工作方式,因為 Compose 是一個聲明式框架,而不是命令式框架。

在聲明性框架中,UI 是使用將數據轉換為用戶界面的轉換構建的。 每當轉換觀察到的數據發生變化時,轉換都會重新運行,結果中的任何更改都會反映在 UI 中。

在聲明性框架中,轉換描述了應該為 UI 提供哪些數據,添加、刪除或更改 UI 的唯一方法是通過更改轉換觀察到的數據來修改轉換產生的內容。

換句話說,命令式框架中的事物是用動詞(創建、修改、刪除)來描述的。 一個聲明性的框架,它是一個名詞。 也就是說,它描述了 UI 是什么,而不是如何創建它。 當轉換改變它對 UI 是什么的想法時,UI 也會隨之改變。 無需描述如何到達那里,那是框架的工作。

轉換的編碼方式、它產生的內容、更改的檢測方式以及轉換重新執行的時間和方式在每個聲明性框架中都各不相同。

在 Compose 中,轉換是函數,觀察到的數據作為參數傳遞給這些函數。 UI 由可組合的 function 的調用控制並且只能通過調用來更改。

轉換 function(大部分)是同步執行的,調用的結果是 UI。 在上面,您在合成完成后在回調 function 中調用placeMarker 由於它不作為組合的一部分調用,因此編譯器將其標記為錯誤。 可組合項 function 只能從另一個可組合項 function 調用,因為結果必須是組合的一部分。 單獨調用它很像將兩個數字加在一起,如a + b ,但不將結果存儲在任何地方。 當您調用可組合項 function 時,您是在說“此 function 的內容位於此處”,這僅在從另一個可組合項 function 調用時才有意義。因此,編譯器會檢查並報告何時在執行以下操作的上下文中調用可組合項 function沒有意義。

請記住,可組合的 function 可以任意多次運行,並且應該總是從相同的數據中產生相同的結果。 考慮一下,隨着數據的每次更改,所有可組合的功能都會重新運行,運行后生成的 UI 就是您看到的 UI,這很有幫助。 Compose 實際上並不運行所有可組合函數(出於性能原因),但它是一個很好的心理 model 擁有。

對上面所做的最簡單的更改是更改onTap以修改MyLayout正在觀察的一些數據,然后根據該數據調用placeMarker或不調用。 此外,由於可組合函數是名詞,而不是動詞,因此它應該被稱為marker 這意味着 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")
        }

    }
}

一旦您熟悉了這個 model ,那么添加一個事件處理程序來刪除標記就相當簡單了,例如,

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

在命令式框架中,這是眾所周知的棘手事情,因為當標記尚未顯示時收到雙擊或收到事件晚時,您需要處理,在樹之后,樹的這一部分已被刪除,等等。所有這些問題都由 Compose 運行時為您處理。

PlaceMarkerOnImage不是從setContent調用的,它是從MyLayout內部調用的。 如果您想將可組合 function 作為參數傳遞給另一個 function,則必須使用@Composable注釋該參數:

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

但這不會解決您的問題,它只會將其移至onTap ,因為detectTapGesturesonTap參數也不接受可組合的 function 。

你要做的是這樣的:

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