One Off the Slack: Is It Necessary to Remember?

Sometimes, these Slack threads get a bit long… though, in this case, there was a fair amount of discussion about young children and their impacts on early-morning work.

Bradleycorn had this composable:

fun FancyText(text: String) {
    val formattedText = remember(text) { computeTextFormatting(text) }

His question was:

Is the use of remember necessary/valuable here? Seems like the only time this would get recomposed is if text changes, and in that case we’d want to re-compute the formattedText value. In other words, we’ll always re-compute the formattedText value, so do we even need remember?

Dominic Fisher pointed out that there are other parties involved:

There are ambients in the background that can also change.

Google’s Adam Powell pointed out that remember is not strictly for performance-related caching:

Reach for remember if you need the properties of the same object outliving a single composition and retaining its identity across those compositions

remember can be very useful for performance optimizations via caching, but there are subtleties. For example, it compares keys via .equals to determine whether it should recompute the value by running the block. Sometimes this can be as expensive or more than just running the operation every time; consider a very deep tree of data classes or large collections where each element will be compared

so when it comes to caching for performance using remember, don’t do it yet, but it’s there if you find you need it.

Dominic then inquired about derivedStateOf():

Can val formattedText = remember(text) { computeTextFormatting(text) } be replaced with derivedStateOf { computeTextFormatting(text) } ?

Google’s Jim Sproch said “no”:

No, derivedStateOf doesn’t do any caching; it merely provides a value that subscribes to any [State object] reads and will update its self when a dependency changes. So remember and derivedStateOf do orthogonal things.

When pressed for an example of derivedStateOf(), Jim offered:

var a by mutableStateOf(0)
var b by mutableStateOf(0)
val sum = derivedStateOf { a + b }

If either a or b changes, then sum will also automatically change. No memory involved, yes updates/subscriptions involved.

Dominic countered:

Why not just do val sum = a + b?

Presumably, Dominic meant in the context of a composable. Compose is tracking the states, so it will re-run a composable function where a or b are detected to change. Jim pointed out that this is not the only scenario:

Another important property is that derivedStateOf can be used outside of composition to build all sorts of fun data structures that cache computations before passing them into composables.

And Adam pointed out the relationship between these things and snapshots:

in most cases, that’s much simpler… as long as sum is only read in the local part of the composition. An important property of derivedStateOf objects is that they change as part of the same snapshot transaction as their inputs; readers won’t see a state of the world where a or b change but sum hasn’t been updated yet. And if those inputs do change, only places where sum is read will invalidate, not the place where it was computed.

class FancyState {
  var a by mutableStateOf(0)
  var b by mutableStateOf(0)
  val result by derivedStateOf {
    somethingVeryExpensiveWith(a, b)

So if you have [the above FancyState] class, then the expensive operation will be cached and only run when needed, and conceptually as part of the same snapshot transaction where a or b changed

so it’s kind of like an observable by lazy that can change over time

The thread continues on the next day to look at an updated sample from Bradleycorn and review comments from Jim and Dominic — check out the whole thread to see how it turns out!

Read the original thread in the kotlinlang Slack workspace. Not a member? Join that Slack workspace here!