https://kotlinlang.org logo
#compose
Title
# compose
b

Bradleycorn

12/05/2020, 3:33 PM
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

Dominaezzz

12/05/2020, 3:43 PM
There are ambients in the background that can also change.
b

Bradleycorn

12/05/2020, 3:46 PM
So then, is it fair to say that you should always
remember
any (non-trivial) calculated values?
d

Dominaezzz

12/05/2020, 3:47 PM
Especially if it's particularly expensive. You don't want to cache everything.
b

Bradleycorn

12/05/2020, 3:51 PM
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

Dominaezzz

12/05/2020, 3:59 PM
That one is a bit more tricky.
Invalidation won't occur if the list is mutated.
Unless a new list is given.
a

Adam Powell

12/05/2020, 4:01 PM
Use remember for persistence across recompositions first and performance when you can measure a meaningful difference
1
j

jim

12/05/2020, 4:03 PM
@Adam Powell typo, or is it too early for me to be parsing english?
a

Adam Powell

12/05/2020, 4:03 PM
nah, might also be not enough coffee yet for me to be writing it though 😄
d

Dominaezzz

12/05/2020, 4:04 PM
What time is it where you are?
j

jim

12/05/2020, 4:04 PM
8am
a

Adam Powell

12/05/2020, 4:04 PM
~8am. My kids actually let me sleep in a little today
d

Dominaezzz

12/05/2020, 4:04 PM
Woah! That's super early.
a

Adam Powell

12/05/2020, 4:05 PM
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

Bradleycorn

12/05/2020, 4:05 PM
Luckily, mine have a babysitter video games, so I can sleep in. Made it all the way to 9am today! 😃
😄 2
a

Adam Powell

12/05/2020, 4:06 PM
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

Dominaezzz

12/05/2020, 4:10 PM
Can
val formattedText = remember(text) { computeTextFormatting(text) }
be replaced with
derivedStateOf { computeTextFormatting(text) }
?
j

jim

12/05/2020, 4:14 PM
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

Dominaezzz

12/05/2020, 4:16 PM
Ohhh, I was so confused at first.
Okay that makes sense I think.
b

Bradleycorn

12/05/2020, 4:17 PM
@jim can you give an example? I’ve not come across
derivedStateOf
yet …
j

jim

12/05/2020, 4:18 PM
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

Dominaezzz

12/05/2020, 4:19 PM
Why not just do
val sum = a + b
?
b

Bradleycorn

12/05/2020, 4:20 PM
ah ok, so it derives a new state from some other state(s) … who’d have guessed that from the name? 😉
😄 2
👆 1
a

Adam Powell

12/05/2020, 4:21 PM
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

jim

12/05/2020, 4:23 PM
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

Dominaezzz

12/05/2020, 4:24 PM
Ah the last two points there really hit the spot. Thanks for explaining.
a

Adam Powell

12/05/2020, 4:25 PM
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

Dominaezzz

12/05/2020, 4:27 PM
Ohh, like literally used outside of composition. I was imagining passing the state outside for some reason.
👆 1
Neat
😁 1
a

Adam Powell

12/05/2020, 4:28 PM
things like this can also combine with
snapshotFlow {}
for other consumption outside composition if you'd like
d

Dominaezzz

12/05/2020, 4:29 PM
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

Adam Powell

12/05/2020, 4:31 PM
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

jim

12/05/2020, 4:31 PM
😫 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

Bradleycorn

12/05/2020, 4:39 PM
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

jim

12/05/2020, 5:48 PM
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

Bradleycorn

12/05/2020, 5:48 PM
you mean this?
Copy code
val postList by remember(viewModel) {
        // A Flow of posts from a room DB.
        viewModel.recentPosts
    }.collectAsState(initial = listOf())
j

jim

12/05/2020, 5:49 PM
Yes
b

Bradleycorn

12/05/2020, 5:50 PM
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

Dominaezzz

12/05/2020, 5:51 PM
I remember (😛) that. It had to do with applying transformations in the composable.
b

Bradleycorn

12/05/2020, 5:51 PM
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

Dominaezzz

12/05/2020, 5:52 PM
Might want to consider using
produceState
I think.
j

jim

12/05/2020, 5:53 PM
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

Bradleycorn

12/05/2020, 5:54 PM
@Dominaezzz .. I could use
produceState
Copy code
val postList by produceState(initial = listOf(), viewModel) {
        viewModel.recentPosts.collect. { value = it }
    }
👍 1
j

jim

12/05/2020, 5:58 PM
produceState
is marginally better I suppose, still does a remember internally.
b

Bradleycorn

12/05/2020, 5:58 PM
@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

jim

12/05/2020, 5:59 PM
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

Adam Powell

12/05/2020, 6:12 PM
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

Bradleycorn

12/05/2020, 6:12 PM
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

jim

12/05/2020, 6:13 PM
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

Bradleycorn

12/05/2020, 6:16 PM
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

jim

12/05/2020, 6:25 PM
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

Bradleycorn

12/05/2020, 6:29 PM
yeah, a world in which I’m not collecting Flows or observing LiveData’s directly in my composables sounds right.
c

Colton Idle

12/05/2020, 9:33 PM
@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

Adam Powell

12/05/2020, 9:42 PM
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

Bradleycorn

12/05/2020, 11:20 PM
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

bruno.aybar

12/06/2020, 12:10 AM
As an additional data point, not sure if it's the correct approach or not, I've been doing the same as @Bradleycorn
9 Views