Why Jetpack compose is triggering unusual recomposition?

76 Views Asked by At

I was working on a project and i faced an issue. Here is the minimal example of my issue in the below code when text changes the TextField is supposed to recompose only. But it recomposing the whole composable function. Why and how to deal with that ?

@Composable
fun RecompositionPractice() {
    var text by remember {
        mutableStateOf("")
    }

    Box { 
        TextField(value = text, onValueChange = { text = it })
    }
}

Here is another example of code where i'm facing same issue. In the below code as well TextField is supposed to recompose but CustomField is also recomposing.

    @Composable
    fun MainScreenContent() {
        var userName by remember {
            mutableStateOf("")
        }
        
        CustomField(value = userName, onValueChange = { userName = it })
        
    }
    
    
    @Composable
    fun CustomField(
        value: String,
        onValueChange: (String) -> Unit
    ) {
        TextField(
            label = { Text(text = "Enter Your Name") },
            value = value, 
            onValueChange = { onValueChange(it) }
        )
    }

Please provide the solution to both code samples.

1

There are 1 best solutions below

0
Милош Којадиновић On

I would say that your code is completely fine, and that you only need to understand why it is like it is.

Composable is recomposed each time mutable state that is read inside of it is changed, this also cause recomposition of children (but recompostion of children that do not use changed mutable state will be skipped). For example if we wrap your code a bit:

@Composable
fun MainContent1() {
    MainContent2()
}

@Composable
fun MainContent2() {
    var userName by remember {
        mutableStateOf("")
    }

    MainScreenContent3(userName, onValueChange = { userName = it })
}

@Composable
fun MainScreenContent3(
    value: String,
    onValueChange: (String) -> Unit
) {
    CustomField(value = value, onValueChange = onValueChange)
}

MainContent1 will not be recomposed, but, MainContent2 and MainContent3 and CustomField (children of MainContent2) will be recomposed because MainContent2 read mutable state which is changed. Way to reduce composing is to move username deeper in composable, so if code is like this:

@Composable
fun MainContent1() {
    MainContent2()
}

@Composable
fun MainContent2() {
    MainScreenContent3()
}

@Composable
fun MainScreenContent3(
) {
    var userName by remember {
        mutableStateOf("")
    }

    CustomField(value = userName, onValueChange = { userName = it })
}

Now MainContent1 and MainContent2 will not be recomposed, but MainContent3 will be. Other option to accomplish the same thing is to pass MutableState, but note that that is not really best solution:

@Composable
fun MainContent1() {
    MainContent2()
}

@Composable
fun MainContent2() {
    val userName = remember { mutableStateOf("") }

    MainScreenContent3(userName)
}

@Composable
fun MainScreenContent3(
    value: MutableState<String>
) {
    CustomField(value = value.value, onValueChange = { value.value = it })
}

But you should check that if you had two text fields like this:

@Composable
fun MainScreenContent() {
    var userName by remember {
        mutableStateOf("")
    }
    var userName2 by remember {
        mutableStateOf("")
    }
    Column {

        CustomField(value = userName, onValueChange = { userName = it })
        CustomField(value = userName2) { userName2 = it }
    }

}

And type something in one field, you would expect that recomposition of other field is skipped, which should be the case, and it should look something like this. You can see that MainContent is recomposed, and one CustomField, but recomposition of other field is skipped.

enter image description here

Hope this helps you, and that you will know what to expect in recompositions now.