I've been playing with Jetpack Compose Desktop. I noticed something I really don't understand:
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
@Composable
@Preview
fun App() {
var text by mutableStateOf("Hello, World!")
MaterialTheme {
TextField(text, onValueChange = { text = it })
Button(onClick = {
text = "Hello, Desktop!"
}) {
Text(text)
}
}
}
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}
Why am I able to change the text in the TextField
? I thought that on every recompose the mutable state get reinstantiated with the initial value: so the Text
should not be able to change
import androidx.compose.desktop.ui.tooling.preview.Preview
import androidx.compose.foundation.layout.Column
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
@Composable
@Preview
fun App() {
var text by mutableStateOf("Hello, World!")
Column {
TextField(text, onValueChange = { text = it })
Button(onClick = {
text = "Hello, Desktop!"
}) {
Text(text)
}
}
}
fun main() = application {
Window(onCloseRequest = ::exitApplication) {
App()
}
}
However, if you replace the MaterialTheme
with a Column
it suddenly works as expected and you aren't able to change the text in the TextField
.
Why is that? Is that a bug or a feature?
It's a feature of Compose about scoping and smart recomposition. You can check my detailed answer here.
What really makes your whole Composable
recompose is that Column
is defined using the inline
keyword.
@Composable
inline fun Column(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
val measurePolicy = columnMeasurePolicy(verticalArrangement, horizontalAlignment)
Layout(
content = { ColumnScopeInstance.content() },
measurePolicy = measurePolicy,
modifier = modifier
)
}
Let's say you have a Composable
that sets the background color initially at composition and changes it at each recomposition which creates its own scope. A lambda without inline
is considered a scope.
@Composable
fun RandomColorColumn(content: @Composable () -> Unit) {
Column(
modifier = Modifier
.padding(4.dp)
.shadow(1.dp, shape = CutCornerShape(topEnd = 8.dp))
.background(getRandomColor())
.padding(4.dp)
) {
content()
}
}
With
@Composable
fun App2() {
var text by mutableStateOf("Hello, World!")
RandomColorColumn {
TextField(text, onValueChange = { text = it })
Button(onClick = {
text = "Hello, Desktop!"
}) {
Text(text)
}
}
}
And one with Column
@Composable
fun App() {
var text by mutableStateOf("Hello, World!")
Column(modifier = Modifier.background(getRandomColor())) {
TextField(text, onValueChange = { text = it })
Button(onClick = {
text = "Hello, Desktop!"
}) {
Text(text)
}
}
}
Random color function
fun getRandomColor() = Color(
red = Random.nextInt(256),
green = Random.nextInt(256),
blue = Random.nextInt(256),
alpha = 255
)
You will see that Column background color will change every time you change text but not with App2()