Travis Griggs
04/26/2023, 11:36 PM@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:
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?Alex Vanyo
04/27/2023, 12:11 AMlastTimestamp
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:
@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) } }
}
Alex Vanyo
04/27/2023, 12:14 AMTimeoutOverlay
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:
val elapsed: Duration by remember(lastTimestamp) { derivedStateOf { currentTime.value - lastTimestamp } }
or you can turn lastTimestamp
back into a state read with `rememberUpdatedState`:
val currentLastTimestamp by rememberUpdatedState(lastTimestamp)
val elapsed: Duration by remember { derivedStateOf { currentTime.value - currentLastTimestamp } }
Alex Vanyo
04/27/2023, 12:16 AMtimeout
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.Stylianos Gakis
04/27/2023, 3:09 PMTravis Griggs
04/27/2023, 3:37 PMlastTimestamp: 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 }
.Travis Griggs
04/27/2023, 3:45 PMStylianos Gakis
04/27/2023, 3:46 PM