kotlinandroid-jetpack-composemutablestateof

Why does mutableStateOf without remember work sometimes?


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?


Solution

  • 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()

    enter image description here