Search code examples
androidandroid-jetpack-compose

derivedStateOf vs remember with key and MutableState


Is there any difference between the following two functions:

First function with derivedStateOf:

@Composable
private fun CounterWithDerivedState() {
    val counterState = remember { mutableStateOf(0) }

    val showFinallyState = remember {
        derivedStateOf { counterState.value > 10 }
    }

    Button(
        onClick = { counterState.value = counterState.value + 1 }
    ) {
        Text(counterState.value.toString())
    }

    if (showFinallyState.value) Text("Finally!")
}

Second function with remember, key and MutableState:

@Composable
private fun CounterWithRemberMutablState() {
    val counterState = remember { mutableStateOf(0) }

    val showFinallyState by remember(counterState) {
        mutableStateOf(counterState.value > 10)
    }

    Button(
        onClick = { counterState.value = counterState.value + 1 }
    ) {
        Text(counterState.value.toString())
    }

    if (showFinallyState) Text("Finally!")
}

I would expect the same behavior in terms of recomposition.


Solution

  • Edit:
    As it turned out, the information provided at the sources (#1 #2) I was basing my answer upon were were not completely accurate. The answer below includes my latest findings made using the Layout Inspector.

    Please check out the documentation of remember:

    Remember the value returned by calculation if key1 compares equal (==) to the value it had in the previous composition, otherwise produce and remember a new value by calling calculation.

    If you use the declaration

    val showFinallyState by remember(counterState) {
        mutableStateOf(counterState.value > 10)
    }
    

    then the value of showFinallyState will be recomputed each time the counterState changes. However, when the output of the calculation actually has not changed from the previous value, the Composables depending on showFinallyState will not recompose. That means that the first recomposition will happen after counterState is > 10.

    Now let's check check the documentation of derivedStateOf:

    You should use the derivedStateOf function when your inputs to a Composable are changing more often than you need to recompose.

    In your first code snippet

    val showFinallyState = remember {
        derivedStateOf { counterState.value > 10 }
    }
    

    you will only get a recomposition of all Composables depending on showFinallyState once the counterState actually is > 10.

    So actually, the two approaches result in the same amount of recompositions in dependant Composables.


    I used the following Composable to investigate the cause:

    @Composable
    fun SampleComposable() {
    
        val counterState = remember { mutableStateOf(0) }
    
        val rememberKeyVariable = remember(counterState.value) {
            mutableStateOf(counterState.value > 10)
        }
    
        val derivedStateVariable = remember {
            derivedStateOf { counterState.value > 10 }
        }
    
        Column(
            modifier = Modifier.fillMaxSize(),
            horizontalAlignment = Alignment.CenterHorizontally,
            verticalArrangement = Arrangement.Center
        ) {
    
            Button(
                onClick = { counterState.value = counterState.value + 1 }
            ) {
                Text("INCREASE COUNTER")
            }
            Text(text = "rememberKeyVariable: ${rememberKeyVariable.value}")
            Text(text = "derivedStateVariable: ${derivedStateVariable.value}")
        }
        
    }
    

    This is the output of the Layout Inspector after clicking the Button eleven times:

    Layout Inspector

    In the Layout Inspector, we can see that both Text Composables actually can skip ten recompositions, until finally the counterState is > 10, at which point they recompose.

    Feel free to comment in case you find any flaws with the setup of the experiment.