I have a question regarding compose and some diffe...
# compose
s
I have a question regarding compose and some differences between different ways to use
remember
,
mutableStateOf
,
derivedStateOf
where all of them seem like they’d do the same thing-ish but with slight differences and potential premature-optimizations by me? More on Thread 🧵
My use case is that I have a composable that takes a timeStamp of when something was submitted
submittedAt: Instant
and I want to show on the UI the time passed since then. I can now do this in all these three ways and it all works.
Copy code
@Composable
fun MyComposable(submittedAt: Instant) {
    val now by currentTimeAsState(updateIntervalInSeconds = 1L)
    val someText1 = remember(submittedAt, now) {
        getRelativeTimeSpanString(submittedAt, now)
    }
    val someText2 by remember(submittedAt, now) {
        mutableStateOf(getRelativeTimeSpanString(submittedAt, now))
    }
    val someText3 = remember(submittedAt, now) {
        mutableStateOf(getRelativeTimeSpanString(submittedAt, now))
    }
    val someText4 by remember {
        derivedStateOf { getRelativeTimeSpanString(submittedAt, now) }
    }
    LogCompositions("RECOMPOSING!")
    Text(text = someTextX) // on someText3 I used `someText3.value` instead
}
As I understand for options #: 1. A simple remember with the keys that
getRelativeTimeSpanString
relies on so that when either of them change the
getRelativeTimeSpanString
function is called again and updates
someText1
. This will be recalculated every time
now
changes which in this case is every second (could be faster too in some other use case) therefore we get a bunch of useless recompositions. The
LogCompositions
functions confirms that. 2. I am not even sure if I was expecting a difference here, but I didn’t get one anyway. I am assuming I would define something like this if it were a
var
that was also mutated inside this composable, not even sure how different this is from #1. 3. Same thing, unsurprisingly 4. This should read
submittedAt
and
now
from the snapshot automatically and only update
someText4
when the result of
getRelativeTimeSpanString
is different from the previous result, therefore acting as some sort of conflate method that flows have, therefore doing recompositions only when it’s needed?
LogCompositions
does confirm that recompositions only happen when the text changes in the end. Bonus points for not even having to be explicit about the keys since
derivedStateOf
has some internal check as I understand. So in conclusion, should we always go for option #4 then? Since
derivedStateOf
isn’t free either, is this internal check that
derivedStateOf
does so expensive that we shouldn’t always do it? How would we make an informed decision on this, I am unsure. Note that even on options #1-#3
MyComposable
did not recompose when the
someText
was read inside another composable down lower that was not inline, therefore it had its own “smaller” recompose scope. But since it only work in that case it feels like a thing that I don’t want to think about in general, and most of the time this isn’t even the case.
Appendix of the functions that you see in the code above ^^
Copy code
@Composable
fun currentTimeAsState(
    updateIntervalInSeconds: Long = 1L,
    clock: Clock = Clock.systemUTC(),
): State<Instant> {
    return produceState(initialValue = Instant.now(clock)) {
        while (isActive) {
            delay(updateIntervalInSeconds * 1_000)
            value = Instant.now(clock)
        }
    }
}

fun getRelativeTimeSpanString(
    from: Instant,
    to: Instant = Instant.now(),
): String {
    return DateUtils.getRelativeTimeSpanString(
        from.toEpochMilli(),
        to.toEpochMilli(),
        DateUtils.SECOND_IN_MILLIS
    ).toString()
}

// Taken from <https://www.jetpackcompose.app/articles/donut-hole-skipping-in-jetpack-compose>
@Composable
inline fun LogCompositions(msg: String) {
    if (BuildConfig.DEBUG) {
        val ref = remember { Ref(0) }
        SideEffect { ref.value++ }
        d { "Compositions: $msg ${ref.value}" }
    }
}

class Ref(var value: Int)
a
It's probably better to use
derivedStateOf
when either: 1. the calculation is expensive 2. the derived state changes less frequently than the original state(s) changes Otherwise you can just use simple calculation without
remember
. Since your case falls into category 2, I would say pattern 4 is the best. Pattern 2 and 3 are meaningless as you are never changing the value of the mutable state.
s
Okay so at least I am happy that it seems like I understand 2-3 correctly, I’d only want to wrap it like that if I were to change that value as you said. For point #1, isn’t this a point that one would make to just use
remember
though? With this wording alone, I’d first think of using a simple remember if all I knew about my use case is that it’s “expensive” This does fall into the point #2 you made though, it’s just that it’s not what first comes to my mind when dealing with such cases. I guess it’s a pattern that I’ll have to learn to spot by intuition? It’s definitely non-obvious in my head still.
c
remember { }
is intended to be used for values that need to be kept constant across recompositions. In this case, anytime
now
changes, you want to recompute the elapsed time and thus do not want it remembered across recompositions. So that right there should be a signal that #1-3 are not a good fit, and
derivedStateOf { }
is what you want to use, or else just compute it directly if it's a fast calculation.
derivedStateOf { }
has internal mechanisms for detecting when it is reading
State
values, and only changes itself when those values themselves are changed. It's less about being tracked across recomposition, and more about optimizing chains of
State
variables that depend on each other. One huge difference between
remember { }
and
derivedStateOf { }
is that the former is
@Composable
while the latter is not. You can stick a call to
derivedStateOf { }
in other layers of your application (not just the
@Composable
layer), for example, a ViewModel that makes computations based on
State
variables. It's probably not the best practice to do that often (it's still a Compose API and you generally don't want to couple your other layers to Compose), but in some specific scenarios it may be an invaluable tool.
And one other note about the subtleties of these functions:
mutableStateOf()
doesn't work without
remember { }
. It can only track its changes internally if the
State
itself is remembered across compositions. You could also hold those
State
objects in some other class that lives outside of Compose, such as your ViewModel, and not need the call to
remember { }
since the ViewModel keeps the state alive across recomposition. Also,
State
objects in general can always be assigned to variables by delegate, so the differences between #2 and #3 are purely syntactic, but do the exact same thing. The implementation of the
by State<T>
delegate it literally just an inlined call to
.value
, so there is no runtime difference between one way or the other
a
Just another point to subtleties: #4 has a very subtle bug as written.
derviedStateOf
can only track changes to
State
objects that it reads in its block.
now
is one such state (due to the delegate), but
submittedAt
is not: it’s a plain value passed in as an argument. Because of that,
derivedStateOf
will continue to use the same, stale
submittedAt
value even if
MyComposable
recompose with a different
submittedAt
value because
remember
has no keys. There are two ways to get correct behavior from this: Adding a key to
remember
for `submittedAt`:
Copy code
val someText4 by remember(submittedAt) {
        derivedStateOf { getRelativeTimeSpanString(submittedAt, now) }
    }
Using `rememberUpdatedState`:
Copy code
val currentSubmittedAt by rememberUpdatedState(submittedAt)
    val someText4 by remember {
        derivedStateOf { getRelativeTimeSpanString(currentSubmittedAt, now) }
    }
(the example at https://developer.android.com/jetpack/compose/side-effects#derivedstateof has the same bug right now, we’re fixing that!)
I would treat
derivedStateOf
like a caching tool: It can be very useful in some situations, but you then also need to be extremely careful with how that caching gets invalidated. In most cases you’d probably be better off just doing the calculation directly.
s
That is incredibly interesting actually. Would this bug stop existing if
submittedAt
was passed into this function as a
State<Instant>
instead of an
Instant
?
a
I though the same at first, but no, there’s the exact same issue (although made even more subtle). The
derivedStateOf
will capture the old
State<Instant>
instance. If you just changed the value of the same
State<Instant>
instance, it might work, but if
MyComposable
got recomposed with a different
State<Instant>
instance passed into it, the
derivedStateOf
will still be stuck listening for changes on the old one. (separately, having
[Mutable]State<Instant>
as a parameter is frowned upon: https://kotlinlang.slack.com/archives/CJLTWPH7S/p1637855772048800?thread_ts=1637854750.046000&amp;cid=CJLTWPH7S)
s
Wow I see, that makes sense now the way that you explain it 🤯 All these subtleties are quite interesting, and now I'm curious why Adam suggests
() -> T
too. What's the use case for that over just
T
? Interestingly, in this case if I had
() -> T
it's more likely that I'd intuitively use
updatedState
since I'm used to seeing it being used with lamdas and it makes sense that it'd be stale inside
derivedStateOf
but now I see that this can be true for normal variables too 🤔
a
If you have a callback or some other object that wants to “get” the current value of some state, that can be different than what the value of the object was when the callback was composed. One way to do that would be to pass the state reference via
State<T>
, for calling
State<T>.value
at some later point in time, but it’s preferred to just pass in the direct
() -> T
lambda for that case. And yup, I was in the same boat of thinking
rememberUpdatedState
was just for lambdas, but it’s a more general tool to convert a parameter for a
@Composable
back into a
State<T>
that can be observed with deferred reads like any other state further downstream. If you wanted to do that manually without
rememberUpdatedState
, I bet you’d get pretty close to the exact implementation of it: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]ate.kt;l=295-297;drc=44c23a7214a34d089ac218acb9460a66e5405c12
👏 1
s
This conversation has been eye-opening for me, thank you so much to all of you for contributing in with your ideas and knowledge!
🎉 1