A question about whether to `remember` something o...
# compose
s
A question about whether to
remember
something or not. More in thread: 🧵
👀 3
Given a Composable like this:
Copy code
@Composable
private fun Title(
    title: String,
    locale: Locale,
) {
    Text(
        text = title.uppercase(locale),
    )
}
From what I understand, if this recomposes for whatever reason,
.uppercase
will be called again (uppercase may potentially not be too expensive but I don’t know that right now. Also the function could be something a bit more expensive in another scenario). If this happens to be inside a composable that for example animates, therefore recomposes multiple times it could be called rapidly many times. So if we change it to something like this:
Copy code
@Composable
private fun Title(
    title: String,
    locale: Locale,
) {
    val titleText = remember(title, locale) {
        title.uppercase(locale)
    }
    Text(
        text = titleText,
        style = MaterialTheme.typography.overline
    )
}
This should only change when either
title
or
locale
change. So the question are: • If this was part of a big composable, is this an optimization that one would try and do, or is this considered premature optimization? (I don’t like the extra code complexity, it can get out of hand if we do that for every single thing) • If this is a simple leaf node of some composable. If that composable recomposes a lot of times per second without changing
title
or
locale
, is compose going to skip recomposing this leaf composable because it can see that it doesn’t need to?
👀 1
z
Answering in reverse: • If Locale is inferred or marked as stable, and it doesn't changed, then yes this function will be skipped (String is inferred as stable, idk about Locale). • It is certainly a potential optimization . Remember isn't free either though, so you'd probably need to profile to see which one is actually faster given typical values of title+locale. • If you have a big composable with an untenable number of remembers, it's a usually a sign that it's time to factor out a state holder class .
Wow, incredible formatting slack. Just brilliant
🥲 1
😂 2
s
Interesting distinction about things being inferred as stable. Where can I read more about understanding how compose looks at it differently from a normal non-mutable class. Does it have to check if it itself has any mutable variables inside it? Note: This is
java.util.Locale
I’m working with, is there a way for me to debug and see if compose does in fact infer if it’s stable or not? I’ve seen the proposal about extracting out a state holder class to make the code look a bit cleaner. Do you know if there are any of the new compose codeLabs that cover this? I am afraid I don’t really know when I should start thinking about it and how to do it properly.
z
Here's some info on stable inference: https://developer.android.com/jetpack/compose/lifecycle#skipping I believe you can explicitly annotate individual parameters as stable if the compiler doesn't infer them correctly, but I can't remember off the top of my head whether that's supported. There is a compiler flag you can turn on that dumps the result of stable inference, lemme find it…
🙏 1
Idk if there are code labs for extracting state holders but a good place to start is just make a private class in the same file, move all your remembered vals into it as class properties, and remove the remember (but keep the mutableStateOf). Then in your composable just remember an instance of that class.
s
So a normal (non-data) class with a bunch of mutableState class properties, interesting. And then in that case if each of those variables were using different keys in their own remembers, now they would all need to be merged together in the new remember call. This is a mild tradeoff I guess.
z
Try this:
-Pandroidx.enableComposeCompilerReports=true
Yes, and pass them all into the constructor. Although if that is very complex too, there are other ways to refactor the code. It might be better to give your state holder one or more update functions called from the composition that take the properties, do their own diffing, and update something. Or you could pass the properties in as state values (e.g. from
rememberUpdatedState
). Or depending on the logic maybe
derivedStateOf
can be used somehow.
It’s very hard to say in the abstract but there are a lot of ways to factor code once you’ve pulled out a state holder.
s
Hmm interesting, I think it starts making more sense yeah. It will make even more when I get to try out doing it myself too, I can see how to approach it though.
As far as the
enableComposeCompilerReports
flag goes, I am afraid I am not familiar with working with those before. Even after adding it, where would I be able to access this report? I couldn’t find any documentation on it.
z
😮 `Stable`/`Immutable` affects how remember works too? Isnt it based on equals/hashcode?
z
For the compiler reports, you’ll want to look in your build directory for files ending in i think
-composables.txt
. The contents should be self-explanatory
I don’t think stability affects
remember
– keys are always compared using
equals
IIRC
👍🏽 1
s
I am afraid I can’t get the
enableComposeCompilerReports
flag to output the file for me, not sure if I am not finding the file itself (ran
find . -name '*composables.txt'
) or if I am applying the flag wrong (ran all
./gradlew -Pandroidx.enableComposeCompilerReports=true app:[build|assemble|install]Debug
) 🤕
z
try also doing
-Pandroidx.enableComposeCompilerMetrics=true
as well
s
./gradlew app:buildDebug -Pandroidx.enableComposeCompilerReports=true -Pandroidx.enableComposeCompilerMetrics=true
nop, nothing. I really don’t want to be wasting your time btw, but all of this seems to not be (public) documented behavior for me to look up myself 😕 I feel like I must be messing up somewhere and I don’t even know where
z
Anyway, pretty sure
Locale
isn’t gonna be stable so if you can’t annotate the parameter directly then you could try wrapping it in a data class or something
👍 1
I had the wrong flags, here’s what you should do instead (thanks Leland):
Copy code
compile.kotlinOptions.freeCompilerArgs += listOf(
 "-P", "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=/path/to/folder",
 "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=/path/to/folder",
)
o
It's premature optimization IMO
👌 1
s
The new flags seem to be the correct ones, however I am getting either:
Unsupported plugin option: androidx.compose.compiler.plugins.kotlin:metricsDestination=/reports/compose
Unsupported plugin option: androidx.compose.compiler.plugins.kotlin:reportsDestination=/reports/compose
depending on which one I put first. Do you know if this is only supported by a specific compose version? This comment might be relevant. I tried with 1.0.4 and 1.0.5 but neither of them worked.