I'm confused by something. I have this "overlay" t...
# compose
t
I'm confused by something. I have this "overlay" that I can wrap around some content, that will display a scrim over the content if a timeout has been exceeded.
Copy code
@Composable
fun TimeoutOverlay(
   lastTimestamp: Instant,
   timeout: (Duration) -> Boolean,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   val currentTime = currentTimeAsState()
   val elapsed: Duration by remember { derivedStateOf { currentTime.value - lastTimestamp } }
   val isUnresponsive: Boolean by remember { derivedStateOf { timeout(elapsed) } }
... content() ...
}
When isUnresponsive becomes true, it shows up and shows a time elapsed counter every second. I use it in a parent composable like so:
Copy code
TimeoutOverlay(
   lastTimestamp = valve.rtu.timestamp,
   timeout = { elapsed -> valve.rtu.exceedsTimeout(elapsed) }) { ... }
The valve.rtu.timestamp is a mutableState. So I thought whenever that changed, it would force this to recompose. But it does not. The currentTimeAsState() is working just fine, my counter increments and it shows up only after the timeout function returns true. But then when timestamp updates, it doesn't pick up on that. Only when I scroll the list so that it has to recompose it do I find that it gets a new and updated version of timestamp. I thought the compose function was basically like a derivedStateOf, in that it captures reads of mutableStates and causes them to recompose when they change. What's the nuance I'm missing here?
a
lastTimestamp
is a parameter to
TimeoutOverlay
, so using the value isn’t a state read directly itself. Since the
remember
for
elapsed
doesn’t have any keys, the block inside
remember
will run once, and it will capture the initial
lastTimestamp
value, so if
TimeoutOverlay
recomposes with a new
lastTimestamp
value, it won’t be used anywhere. This is roughly equivalent and might make what’s happening a bit more clear:
Copy code
@Composable
fun TimeoutOverlay(
   lastTimestamp: Instant,
   timeout: (Duration) -> Boolean,
   modifier: Modifier = Modifier,
   content: @Composable () -> Unit
) {
   val currentTime = currentTimeAsState()
   val elapsed: Duration by remember {
        val initialTimestamp = lastTimestamp
        derivedStateOf { currentTime.value - initialTimestamp }
    }
   val isUnresponsive: Boolean by remember { derivedStateOf { timeout(elapsed) } }
}
So
TimeoutOverlay
is recomposing when
lastTimestamp
is changing, but the
remember
is caching an initial value of one of the parameters. For fixing it, you have a couple of options: you could use
lastTimestamp
as a key to the remember:
Copy code
val elapsed: Duration by remember(lastTimestamp) { derivedStateOf { currentTime.value - lastTimestamp } }
or you can turn
lastTimestamp
back into a state read with `rememberUpdatedState`:
Copy code
val currentLastTimestamp by rememberUpdatedState(lastTimestamp)
val elapsed: Duration by remember { derivedStateOf { currentTime.value - currentLastTimestamp } }
You might have a similar issue for the
timeout
lambda: since the
remember
for
isUnresponsive
doesn’t have any keys, it will capture the first
timeout
lambda passed to
TimeoutOverlay
, and even if you pass a new
timeout
lambda,
isUnresponsive
will continue to use the old one.
s
composable parameters not being state but being normal vals has been a very important thing to learn, I still remember this discussion from 2021 https://kotlinlang.slack.com/archives/CJLTWPH7S/p1640293291045100?thread_ts=1640270959.029600&cid=CJLTWPH7S where you explained this to me again. It’s worth reading this thread too Travis.
t
Very good explanation @Alex Vanyo. Thanks so much. This clicked. The derivedStates will create a computation that updates when any of its accessed states update... but by passing accessing the timestamp elsewhere and passing it as a parameter, I had essentially create a temporary variable snapshot that the derivedState closure wasn't going to capture as a "source of change". I solved this (for now) by simply turning the
lastTimestamp: Instant
parameter into a
lastTimestamp: () -> Instant
. For preview and separation purposes that allows me to pass a simple test closure (e.g.
{ Instant.now() }
or a more involved access like
{ gauge.rtu.lastTimestamp }
.
Thanks @Stylianos Gakis. You had shared that before, but I don't think I had enough context to understand it well. Zach's illuminating articles on the Snapshot system and how derivedState essentially takes advantage of a read-bearer to automagically capture dependency graphs has been a bit of "ahah" for me.
s
Yeap, the more context you get, and as you encounter such scenarios yourself, it makes it all much easier to understand. I hope it makes even more sense now 😊