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