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