One Off the Slack: When Do We Use derivedStateOf()?
Steffen Funke asked:
Would
derivedStateOf
be the State-equivalent ofcombine
in Flow / Rx-World? Meaning, if one of the inputs change, the resulting state should be recalculated? And can this also safely be used inside a ViewModel?
Background: I have e.g. a list and a search bar, and using
MutableStateFlows
inside my ViewModel for data streams. Whenever the search term changes, Icombine
the search term with the list, to filter it.
Works well and solid, but I think about going full Compose also in my ViewModels, therefore replacing my
MutableStateFlow
s with Compose States.
Is this the correct approach, using
derivedStateOf
?
Google’s Adam Powell responded:
derivedStateOf
caches the result of a computation based on State accessed during that computation. However, you don’t needderivedStateOf
to combine several different state inputs. It’s just as valid to do this:
class Greeter {
var name by mutableStateOf("World")
var greeting by mutableStateOf("Hello")
val fullGreeting: String
get() = "$greeting $name"
}
with no
derivedStateOf
at all; if you doText(greeter.fullGreeting)
then that will recompose any time either name or greeting change.
The key advantages of
derivedStateOf
are lazy evaluation and caching if the inputs have not changed. This caching isn’t free; if all you’re doing is something like the above, the computation isn’t particularly heavy, or you don’t have many reads offullGreeting
without inputs changing it’s better to keep it simple and just compute on the fly.
Steffen was suitably impressed. But, problems keep getting more complicated, so Steffen dug deeper:
when using
derivedStateOf
inside a ViewModelval
, what would be the best strategy to let the calculation run inside a coroutine?
At the moment I am playing around with a quite an expensive filter logic. With flows, I was able to move that calculation to a background dispatcher with
flowOn
. I could still work around it by keeping this logic as aFlow
, and then populating mymutableStateOf
s from it - but I wonder if there is a better way.
Adam filled in the gaps:
Once you start wanting to compute derived things asynchronously then the tools get more complicated as well. You can use
snapshotFlow
to take a block of code that reads snapshot state and emits from a flow when things change, then do your usual redirection to other dispatchers if you like. At the end of a flow operator chain you can write another snapshot state value as part of your collect. This doesn’t come up often though, since snapshots are generally intended to be atomic. Offloading derived snapshot state calculations to another coroutine dispatcher means you’re probably giving up that atomicity unless you’re doing something pretty sophisticated by hand with the low level snapshot APIs
As for how it works, the current snapshot is a matter of some custom ThreadLocal tracking, so it doesn’t matter where in the call stack you are when a snapshot read or write is performed, it’s tracked.
The compose compiler plugin isn’t involved with snapshots at all
This is also a fine place to plug Zach Klippenstein’s long article on the snapshot system.
Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!