Home > Enterprise >  remember derivedStateOf or not
remember derivedStateOf or not

Time:10-18

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.

CodePudding user response:

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.

CodePudding user response:

I got intrigued by your example, so I tried to dissect it to the best of my understanding, I'll just enumerate them and pardon the repeating words, I also marked those parts that I'm not sure with with assumption

For the first one WITHOUT remember { ... }

moreThanOne.value // code in place

Initial compose pass (count = 0)

  • deriving a 'count' state under-the-hood (own lambda) - assumption
  • Composable scope reads a NEW moreThanOne.value state as "false" (therefore print Read)
  • print Calculate upon creating a derived state (own lambda) - assumption

Second compose pass (count = 1)

  • update count state, prints Clicked
  • derivedState re-calculates due to 'count' being incremented, also prints Calculate (own lambda) - assumption
  • Composable scope reads THE SAME moreThanOne.value state as "false" does not print Read

Third compose pass (count = 2)

  • update count state, prints Clicked
  • derivedState re-calculates due to 'count' being incremented, also prints Calculate (own lambda) - assumption
  • Composable scope reads a NEW moreThanOne.value state as "true", therefore prints Read
  • derivedState recalculates because parent Composable re-composes, it prints Calculate (own lambda) - assumption

Now for the one WITH remember { ... }

 moreThanOne // code in place, though unused reference

Initial compose pass (count = 0)

  • remember calculates new derivedState of count > 1 as State(false) under-the-hood - assumption
  • Composable scope reads a NEW remembered moreThanOne State(false), prints Read
  • print Calculate upon creating remembering/calculating a new derivedState State(false)

Second compose pass (count = 1)

  • update count state , prints Clicked
  • remember detects "count" got incremented, and because "count" is read by a derivedState, it recalculates a new derivedState though still having same state-value State(false) but derivedState saw count changed, therefore print Calculate
  • Composable scope doesn't read any moreThanOne state change therefore Read NOT printed

Third compose pass (count = 2)

  • update count state, prints Clicked
  • remember detects "count" got incremented, and because "count" is read by a derivedState, it recalculates a new derivedState and now count > 1 creates State(true) and prints Calculate
  • Composable scope reads a fresh moreThanOne state change State(true) therefore Read is printed
  • remember doesn't need to re-calculate the derivedState here, because it's already been calculated with State(true) and count > 1, so it won't print Calculate

The key points I learned from here

  • remember reads and calculates based on the derivedState
  • derivedState re-calculates based on the boolean value

And the assumptions I made are

  • derivedStateOf having its own function scope thats why Read prints first
  • if derivedStateOf is inlined, I think we will get a linear print outputs

And the crucial part are the 3rd passes, without wrapping it inside remember {...}, derivedStateOf{...} will do its job re-calculating, therefore in the first case, prints Calculate.

Apologies for the very long answer though, and I didn't know I only need to suppress the lint for me to get past the compile error

  • Related