prat
12/28/2020, 4:24 PMmillis
value doesn't change when I add a log at A - before Canvas. Without the log or when I add log at B instead, millis
gets updated. Is this an expected behavior?
@Composable
fun DrawSomething(
modifier: Modifier,
strokeWidth: Float = 8f
) {
val millis = animationTimeMillis()
// A: when adding a Log here, millis.value will stop at 0 and won't draw arc
// Log.d("DrawSomething", "millis : ${millis.value}")
Canvas(modifier = modifier) {
// B
// Log.d("DrawSomething", "millis : ${millis.value}")
drawArc(
color = Color.Green,
startAngle = 0f,
sweepAngle = 0f + (millis.value / 360),
useCenter = false,
size = Size(100f, 100f),
style = Stroke(width = strokeWidth)
)
}
}
@Composable
fun animationTimeMillis(): State<Long> {
val millisState = mutableStateOf(0L)
val lifecycleOwner = AmbientLifecycleOwner.current
LaunchedEffect(Unit) {
val startTime = withFrameMillis { it }
lifecycleOwner.whenStarted {
while (true) {
withFrameMillis { frameTime ->
millisState.value = frameTime - startTime
}
}
}
}
return millisState
}
Adam Powell
12/28/2020, 9:17 PMval millisState = mutableStateOf(0L)
should be:
val millisState = remember { mutableStateOf(0L) }
DrawSomething
produced an unstable result. Adding a new point of invalidation (reading ${millis.value}
in the log statement) triggered the bug.LaunchedEffect
in animationTimeMillis
uses Unit
as its subject key. This is one of those constructs that is always just a little bit unsafe; when you see the use of Unit
(or true
or any other constant) as an effect subject key and there's a bug at play, there's a good chance this is where it's hiding.while (true)
- plenty of cases where it's appropriate but it should always make you look closelyLaunchedEffect
has two inputs that it closes over that would make good subject keys and would help trace bugs like this: both millisState
and lifecycleOwner
- if either of these instances change, you may want to restart the LaunchedEffect
and stop the old one.millisState
was missing a remember
, each time animationTimeMillis
recomposed, it created a new MutableState<Long>
and returned it, but the LaunchedEffect
was still updating the old state object from that initial composition.
Adding the log statement right after the call to animationTimeMillis
meant that DrawSomething
now recomposes when millis
changes for the first time, causing the animationTimeMillis
call to recompose, triggering the bug.withFrameMillis
loopmutableStateOf
inside of a composable function and suggest surrounding it with a remember {}
.prat
12/28/2020, 10:48 PM