androidkotlinandroid-jetpack-composeandroid-jetpack-compose-ui

remember derivedStateOf or not


In the examples I find (here or here) I see that derivedStateOf is always wrapped in a remember block. Checking the Recomposition counts, I don't see a difference between

val foo = remember { derivedStateOf { someState } }

and

val foo = derivedStateOf { someState }

Could anyone show me an example where the result would differ?

Edit: So I got a different result in this example:

@Composable
fun Test() {
    var count by remember {
        mutableStateOf(0)
    }

    val moreThanOne = derivedStateOf {
        Log.d("foo", "Calculate")
        count > 1
    }

    Log.d("foo", "Read")
    moreThanOne.value

    Button(
        onClick = {
            Log.d("foo", "Clicked")
            count += 1
        }
    ) {
        Text(text = "Increment")
    }
}

Clicking the Button twice gives the following log:

Read
Calculate
Clicked
Calculate
Clicked
Calculate
Read
Calculate

Wrapping the derivedStateOf with a remember however logs:

Read
Calculate
Clicked
Calculate
Clicked
Calculate
Read

Still not completely sure why I see what I see.


Solution

  • Your code should look like,

    @Composable
    fun Test() {
        var count by remember {
            mutableStateOf(0)
        }
    
        val moreThanOne = remember {
            derivedStateOf {
                Log.d("foo", "Calculate")
                count > 1
            }
        }
    
        Log.d("foo", "Read")
        moreThanOne.value
    
        Button(
            onClick = {
                Log.d("foo", "Clicked")
                count += 1
            }
        ) {
            Text(text = "Increment")
        }
    }
    

    Note the additional remember around derivedStateOf. This doesn't affect the results of the logging but the lack of remember around the derivedStateOf requires additional overhead cause by the creating of a new derivedStateOf every time Test runs. You should always remember derivedStateOf when in a composable function.

    From this code I receive the following:

    D/foo: Read
    D/foo: Calculate
    D/foo: Clicked
    D/foo: Calculate
    D/foo: Clicked
    D/foo: Calculate
    D/foo: Read
    D/foo: Clicked
    D/foo: Calculate
    D/foo: Clicked
    D/foo: Calculate
    D/foo: Clicked
    D/foo: Calculate
    

    The first Read and Calculated are from initial composition. The value of count is 0.

    After the first click Calculated is emitted but nothing else. This is cause by Test becoming conditionally invalidated by the change of count. It is conditional in that Test is only called if the calculated value of moreThanOne is different than previous read by Test. In this case it returns false again so Test is not called.

    The second click Calculate is seen again and now moreThanOne produces true which causes the conditional invalidation of Test to be treated as a real invalidation and Test is invoked to update the composition.

    The third and subsequent clicks just produce Calculated as the lambda is invoked (because count changed) but the result of moreThanOne is not different so the conditional invalidation of Test is ignored.

    The reason Calculated appears before Read even though the log of Read is before the expression moreThanOne.value is that the current value of moreThanOne is updated prior to Test being called to see if Test needs to be called at all. If, however, Test was invalidated for some other reason (that is, it read some other state object that changed) then moreThanOne would only be updated by the call to moreThanOne.value. Derived state objects are only updated prior to the call to Test if they are the only reason Test is being requested to be re-invoked. If they return the same value as the prior composition then the invalidation is ignored and the call is skipped.