I have a sort of theory/philosophy question. I hav...
# compose
t
I have a sort of theory/philosophy question. I have a set of model things that include a number of Instants (things like future start times). I have a bunch of composables that render these model things in various ways. When the models (with mutablestated fields) change, the composition just does as it should. Separation of concerns. Model-Render/View separation. Good stuff. I render a lot of "time remaining" values from these instants. But that should update/recompose every second. I'm loath to put that kind of thing in my model code. The data is just a non changing instant. But since the clock is always ticking forward, the compositions need to be continuously updated. It is a function of the ui state. How do others tend to deal with this? Especially if you have a number of diverse widgets that may need to be recomposed on a one second tick because their computed/derivative texts change with time? This may show my newbieness and be a BadIdea, but I've been considering my own LocalProvider with the current Instant and a sideeffect that ticks it every second. Then any composable that needed this kind of computation would reference the current clock, and be automatically triggered to recompose when it changed. Is that a super bad idea?
s
Not quite sure about all the details of what you are describing, but what I’ve done at one place where I had exactly some data which has an Instant (in the past in our case). I’ve made a currentTimeAsState function, which returns a State which updates each second(configurable), and then this state is read inside some `derivedStateOf`s so that it only recomposes when there is an actual change in the resulting texts. Maybe this is something that may assist you in what you are trying to do? No need for a composition local at all to bloat the entire tree with an extra local. Also each call on
currentTimeAsState
can this way configure their own preferred update interval, and it stops ticking when not in composition too. If this is not what you are looking for, I’d be interested to hear what you need instead.
t
This totally assists me. Thank you. If nothing else, it validates my thought to see the clock as state on a different axis (not exactly model, but not exactly render/view either). I love your name for it in fact: currentTimeAsState. I would love to see how you implemented that? (oh, that's a link you shared already!. thanks!)
s
This is why I love that what I work on is open source 😅 You just got the link right there 😄
t
I employed it as follows:
Copy code
val currentTime = CurrentTimeAsState()
val remaining: Duration by remember { derivedStateOf { active.interval.endInclusive - currentTime.value } }
Text(text = "$remaining" ... )
Is that how you use it?
s
Yeah this looks good I think. How we specifically use it is provided in the link above too (or here). In your case, it also depends on what
endInclusive
is, and if that is also mutablestate or not, and if
active
is a parameter that may also change. If you showed me the entire function I might be able to give better help. But the idea is that if
active.interval.endinclusive
is not mutable state, and it’s a parameter which may change, you may need to add it as a key in your
remember
call, so you never reference an old
endInclusive
value.
t
I'm using it with the following code:
Copy code
@Composable
private fun ActiveIntervalRow(
   active: ValveActive, isLocked: Boolean, modifier: Modifier = Modifier
) {
   val boldColor = isLocked.opt(Color(0.8f, 0.8f, 0.8f), active.darkColor)
   val softColor: Color = isLocked.opt(Color.Transparent, active.color.copy(alpha = 0.3f))
   Row(
      modifier = modifier.background(color = softColor),
      verticalAlignment = Alignment.CenterVertically
   ) {
      val currentTime = CurrentTimeAsState()
      val into: Duration by remember { derivedStateOf { active.interval.start - currentTime.value } }
      Text(
         text = into.clockString,
         modifier = Modifier.padding(start = 12.dp),
         color = boldColor,
         fontSize = 12.sp,
         fontFamily = FontFamily(Typeface.MONOSPACE)
      )
      Box(
         modifier = Modifier
            .fillMaxHeight()
            .weight(1f)
            .padding(6.dp)
      ) {
         Box(
            modifier = Modifier
               .background(color = boldColor)
               .align(Alignment.Center)
               .fillMaxWidth()
               .height(1.5.dp)
         )
         Box(
            modifier = Modifier
               .background(color = boldColor)
               .align(Alignment.CenterStart)
               .fillMaxWidth(fraction = active.fraction)
               .height(5.5.dp)
         )
      }
      val remaining: Duration by remember { derivedStateOf { active.interval.endInclusive - currentTime.value } }
      Text(
         text = remaining.clockString,
         modifier = Modifier.padding(end = 12.dp),
         color = boldColor,
         fontSize = 11.sp,
         fontFamily = FontFamily(Typeface.MONOSPACE)
      )
   }
}
(Curious why you didn't capitalize the currentTimeAsState since it was a composable? Is there an additional dimension to the convention I don't understand? Or just the style you like?)
s
Naming of composable functions that do not return
Unit
aka composables that do not emit UI is defined here https://github.com/androidx/androidx/blob/androidx-main/compose/docs/compose-api-guidelines.md#naming-composable-functions-that-return-values This entire document in fact is super interesting, so I suggest you do read through all of it at least once A function named
CurrentTimeAsState
implies that there is some UI that is being generated from this, but this is a lie, since it returns a value instead. It’s very odd looking inside compose code, and I would very strongly suggest you do not do it, to make it easier for other compose devs to better understand your code 😊
Yeah so looking at your code, if
ValveActive
internally stores
start
as mutableState, then this is fine. If not, you’d want to add
active
as a key to your remember, like
Copy code
val into: Duration by remember(active.interval.start) { derivedStateOf { active.interval.start - currentTime.value } }
Since if you do not do that, if
active
changes, this calculation will not be recalculated, as it wouldn’t know to do so. It does when reading
currentTime
since
derivedStateOf
does that automatically when specifically reading state stored inside compose runtime’s MutableState. And then
val remaining: Duration by remember(active.interval.endInclusive) { derivedStateOf { active.interval.endInclusive - currentTime.value } }
too, for the second usage of this My general rule of thumb when deciding on what to put in the keys of a remember, is • what am I reading from inside my remember lambda? • Are any of those things gonna ever change? If they are parameters in the composable then that’s a yes, so that’s an easy inclusion • Are they MutableState? If yes, can I use derivedStateOf to avoid adding that key instead? Something like that 😅
t
What if ValveActive is stored by a higher level object as a mutable state? That's what I'm doing right now. And when that happens, it will recompose this fucction anyway
Or put another way, ValveActive is an immutable data class. It is stored in a mutablestate accessed by the composition that wraps this composition
s
No that by itself wouldn’t help, from the POV of the
ActiveIntervalRow
function,
active
is a normal parameter, not mutable state. Look at this discussion https://kotlinlang.slack.com/archives/CJLTWPH7S/p1640285517040200?thread_ts=1640270959.029600&cid=CJLTWPH7S for some very good explanation on this, and some extra interesting information too.
t
IF you have a whole list of these, do you get a scenario where the per second ticks happen out of phase with each, since you're remember a unique "ticker" per each leaf composition, leading to the values all updating at different times creating an unpleasant visual effect? My thought/reasoning/proposal to use a single shared value passed down to any consumers through a LocalProvider was a thought that that would reduce that kind of flicker... but I have not implemented/tried anything yet
s
Yeah that’s fair, you could get such a visual effect if you got many separate instances of
currentTimeAsState
you could definitely get things updating in different frames. But tbh it sounds like the solution there is to lift that function to the lowest common parent, and drill that one down to the functions that need it. Now if your application somehow uses this so much that you need to read this virtually everywhere, maybe a composition local would be good, but I really would be careful with making unnecessary composition locals whenever possible.
t
Can you elaborate why that is? My guess is that in general it's bad because Globals Are Bad (tm), and I get that. I'm just curious if there's a nuance I'm missing about them beyond that?
I guess my concern with passing it down everywhere, is that I'd have to poke a hole in all slot compositions between these relatively leaf ones and the very top. Wouldn't that cause the whole stack to recompose on every tick? Or is the magic smart enough to figure out that it can just "pass through" the intermediate compositions and update the leaf ones?
s
Yes, basically because of this https://kotlinlang.slack.com/archives/CJLTWPH7S/p1615733442201400?thread_ts=1615732915.200700&cid=CJLTWPH7S. They’re hard to know that they’re there, and for a newcomer to a codebase they’ll basically never reach for that since they gotta know in advance that it exists, otherwise it’s not obvious unless it’s a super common one like LocalContext and other such locals.
t
I didn't know they used to be called Ambients. I like that name. Wish they would have stuck with it. CompositionLocal is kind of an oxymoron. HierarchicallyInheritedCompositionPreloadedVariable would make more sense 😄
s
Regarding having everything recompose, no it won’t do that (with a caveat explained below). Here’s a great article about how compose can skip such compositions and be more performant by doing so https://www.jetpackcompose.app/articles/donut-hole-skipping-in-jetpack-compose Only caveat, if you pass them down as just
Foo
then it will in fact recompose all the way down. But if you pass it down as
() -> Foo
then it won’t and it will only recompose inside the composition scopes that the state is read. More on this in the aforementioned article, and even some mention of it here.
For such optimizations, it helps a lot to understand what a recomposition scope is, and how a state read invalidates such a scope. And how a
() -> Foo
can go around that. Oh and speaking of that, this article https://dev.to/zachklipp/scoped-recomposition-jetpack-compose-what-happens-when-state-changes-l78 is also a must read IMO if you want to understand how all this works. This is maybe a pre-requisite to reading the donut hole skipping article. As it goes through a lot of the other concepts important to compose and recomposition scopes
Kind of an article/link bomb that I am giving to you right now, but all these sources are the ones that have helped me the most to understand all this, and I hope it does the same to you too.
t
sadlly, i'll read through them, and kinda get things, but it takes time and practice for the words to sink in (as you probably know)
thank you for being willing to share
s
I definitely know. I didn’t understand all these things the first time I read through them, that I can tell you for sure 😅 And maybe I still don’t understand all of them 100%, but we’ll get there some time 😄
t
Just following up, I read the entire 7 part series by Zach today. Extremely helpful/informative.
s
It is amazing indeed!