Given this basic composable: ```@Composable fun Fa...
# compose
b
Given this basic composable:
Copy code
@Composable
fun FancyText(text: String) {
    val formattedText = remember(text) { computeTextFormatting(text) }
    Text(formattedText)
}
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
?
πŸ‘€ 1
d
There are ambients in the background that can also change.
b
So then, is it fair to say that you should always
remember
any (non-trivial) calculated values?
d
Especially if it's particularly expensive. You don't want to cache everything.
b
yep. The use case that got me thinking is something kinda like this:
Copy code
@Composable
fun AlphabeticalList(items: List<String) {
    val aList = items.filter { it.startsWith("A") }
    val bList = items.filter { it.startswith("B") }

    ....
}
figured I should probably use
remember(items)
on both of those
d
That one is a bit more tricky.
Invalidation won't occur if the list is mutated.
Unless a new list is given.
a
Use remember for persistence across recompositions first and performance when you can measure a meaningful difference
❓ 1
j
@Adam Powell typo, or is it too early for me to be parsing english?
a
nah, might also be not enough coffee yet for me to be writing it though πŸ˜„
d
What time is it where you are?
j
8am
a
~8am. My kids actually let me sleep in a little today
d
Woah! That's super early.
a
rephrased; reach for remember if you need the properties of the same object outliving a single composition and retaining its identity across those compositions
πŸ‘ 1
b
Luckily, mine have a babysitter video games, so I can sleep in. Made it all the way to 9am today! πŸ˜ƒ
πŸ˜„ 2
a
My 5 year old is currently entertaining my 3 year old and they aren't fighting yet
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 class`es or large collections where each element will be compared
πŸ‘ 2
πŸ‘† 1
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.
πŸ‘† 1
πŸ‘ 1
d
Can
val formattedText = remember(text) { computeTextFormatting(text) }
be replaced with
derivedStateOf { computeTextFormatting(text) }
?
j
No,
derivedStateOf
doesn't do any caching; it merely provides a value that subscribes to any model reads and will update its self when a dependency changes. So
remember
and
derivedStateOf
do orthogonal things.
derivedStateOf
is super useful, but only if the things inside the lambda are reading from models.
Wow, I still call them models. I mean from
State
objects.
d
Ohhh, I was so confused at first.
Okay that makes sense I think.
b
@jim can you give an example? I’ve not come across
derivedStateOf
yet …
j
Copy code
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.
d
Why not just do
val sum = a + b
?
b
ah ok, so it derives a new state from some other state(s) … who’d have guessed that from the name? πŸ˜‰
πŸ˜„ 2
πŸ‘† 1
a
in most cases, that's much simpler, @Dominaezzz 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.
j
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.
βž• 1
d
Ah the last two points there really hit the spot. Thanks for explaining.
a
Right. So if you have some class
Copy code
class FancyState {
  var a by mutableStateOf(0)
  var b by mutableStateOf(0)
  val result by derivedStateOf {
    somethingVeryExpensiveWith(a, b)
  }
}
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
d
Ohh, like literally used outside of composition. I was imagining passing the state outside for some reason.
πŸ‘† 1
Neat
😁 1
a
things like this can also combine with
snapshotFlow {}
for other consumption outside composition if you'd like
d
Woah, I didn't know
snapshotFlow
could be used outside composition!
Time for some refactoring!
πŸ˜„ 1
I've been trying to "subscribe" to
State
and ended up just using
MutableStateFlow
.
a
if you start using
snapshotFlow
do try to avoid round-trip conversions where you end up doing a
.collectAsState
for a flow that started as snapshot state to begin with πŸ™‚
πŸ“ 1
j
😫 becomes sad again that the call syntax for composables is no longer different from the call syntax of non-composables. But alas, no use crying over spilt milk
b
So many tools! 😍 …. so little time to learn them all … which is why I’m sitting at my computer on a Saturday morning.
πŸ˜‚ 3
j
Why do you have that
remember
? That seems wrong. As a general rule of thumb, IMO, if you are using remember outside of a parameter's default value expression, you're probably writing smelly code.
b
you mean this?
Copy code
val postList by remember(viewModel) {
        // A Flow of posts from a room DB.
        viewModel.recentPosts
    }.collectAsState(initial = listOf())
j
Yes
b
I read something on here a few days back (might have been from @Adam Powell) about some gotchas with collecting flows and recomposition, and the suggestion was to use
remember
… Let me see if I can find that.
d
I remember (πŸ˜›) that. It had to do with applying transformations in the composable.
b
Oh, it was in the case where you have a
Flow
and use some operators on it, like
flowOf(1,2,3).map { it * 2}.collectAsState()
in the case of recomposition, it’s a new flow and has to setup a whole new subscription.
d
Might want to consider using
produceState
I think.
j
Also, having the HomeScreen take in an implementation of ViewModel needlessly ties your widget to your application's android view implementation. Better to define an interface that your HomeScreen accepts as a parameter, and your view model can implement that interface, but it allows you to test (or reuse) your HomeScreen in contexts where Android view models are not available.
b
@Dominaezzz .. I could use
produceState
…
Copy code
val postList by produceState(initial = listOf(), viewModel) {
        viewModel.recentPosts.collect. { value = it }
    }
πŸ‘ 1
j
produceState
is marginally better I suppose, still does a remember internally.
b
@jim yeah, I could certainly do without the ViewModel. But just in terms of dealing with State in a situtation like this, does my use of
produceState
,
remember
, and
derivedStateOf
seem like a sane way to go about it …
j
How would you do this if
model.recentPosts
needed to be fast and return a list for consumption by some other API?
(in a non-compose world)
I think the answer would be that you would terminate the flow within the implementation of the model and would hold the last known result in the model, making it available for easy access as an observable list (eg.
State<ImmutableList>
or
SnapshotStateList
). I would do that, and then there is no need for theΒ 
remember
Β in the middle of your composable.
a
That can be a good approach if the model has a natural scope to collect in or if the source is naturally hot. I'd lean towards the cold/produceState approach if you want the upstream subscription to be scoped more tightly to only produce data when it's being observed. Both approaches have their place
b
when you say to terminate the flow in the model, you mean to
collect
it there (and not, use a terminal operator like say,
first()
), right? If the database changes, I need to update state, so …
j
Correct, collect it there.
As Adam points out, there is the question of hotness.Β I agree, this is a challenge we should better address with an improved derivedState API of some sort that better tracks hotness, but in the mean time it's probably ok to leave it hot in this case.
b
Yeah, I wrote it the way I did because of what @Adam Powell said. Particularly since it’s a
ViewModel
I’m working with that is scoped to the Activity, it can be difficult to get a proper scope in the ViewModel that would be canceled, should the user navigate elsewhere and the composable removed from the tree
Also,
produceState
is what I know, so I tend to use it for everything. πŸ™‚ Hence all these questions to try and broaden my knowledge base and toolset.
j
Yeah, it's unfortunate that we haven't had enough time to fully engineer a better derived state solution. At least
produceState
is marginally better than
remember
.
But I would not throw away the idea of terminating flows in your models, as that's closer to what I personally hope we will ultimately provide.
b
yeah, a world in which I’m not collecting Flows or observing LiveData’s directly in my composables sounds right.
c
@jim
Why do you have thatΒ 
remember
?Β  That seems wrong.Β  As a general rule of thumb, IMO, if you are using remember outside of a parameter's default value expression, you're probably writing smelly code.
I'm so worried about writing smelly code. 😒 I know it's inevitable that I will, but I almost with we could just lint check everything that seems even slightly questionable... lol Lint Check: "Are you sure you want to do this, because you probably don't?"
a
remember outside of a default parameter expression isn't something strict enough to where a lint check would be more helpful than annoying, but composables owning private state across compositions does afford some opportunities for writing some code that grows hard to maintain and test over time
asking, "how would I test this composable if I could only compose once?" can be a useful thought experiment too
πŸ’― 1
πŸ‘ 1
πŸ‘† 1
since it draws you to writing composable functions that don't own deferred work that reaches out elsewhere in your system, instead it draws you to hoisted state objects that you can configure and fake into a very specific state easily
cold observables sort of fall into that category of deferred work by definition
being intrinsically suspicious of remember calls in composable function bodies steers you towards separating code responsible for managing data flow from the code responsible for consuming the data produced by those processes. The latter become composables that don't need to do much internal remembering, if any at all.
πŸ‘ 2
πŸ‘† 1
πŸ’― 1
b
To that point, in my (albeit limited) experience, I can almost always hoist up all of the complexities around producing state and `remember`ing state values to a really high level. I usually wind up with a β€œScreen” composable that houses these items, and only these items, and then once a state is produced, just pass it down to a β€œContent” composable that is entirely stateless and just takes in values and renders various widgets. Something like:
Copy code
@Composable
fun MyScreen(...) {
    val someStateValue by produceState(...) 
    val anotherValue by remember { someExpensiveMethod() }

    ScreenContent(someStateValue, anotherValue)
}
This seems to work well for me so far. I can easily create a @Preview of the β€œContent” composable, and I can easily test the β€œContent” composable as well.
πŸ‘ 1
βž• 2
b
As an additional data point, not sure if it's the correct approach or not, I've been doing the same as @Bradleycorn