https://kotlinlang.org logo
#compose
Title
# compose
p

prat

12/28/2020, 4:24 PM
Hi, I'm learning how effect works and trying to understand why
millis
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?
Copy code
@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
}
a

Adam Powell

12/28/2020, 9:17 PM
This line:
Copy code
val millisState = mutableStateOf(0L)
should be:
Copy code
val millisState = remember { mutableStateOf(0L) }
There are a bunch of neat compose principles at play here! Here's how to look for and prevent these kinds of bugs in the future:
The key insight is that you found a case where a recomposition of
DrawSomething
produced an unstable result. Adding a new point of invalidation (reading
${millis.value}
in the log statement) triggered the bug.
This always means that something in your composable functions isn't idempotent. When effects are at play it's always important to examine your lambda captures
The
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.
think of it like a
while (true)
- plenty of cases where it's appropriate but it should always make you look closely
in this case, it means that the effect only launches once, regardless of what was captured in its trailing lambda for that initial composition
But this
LaunchedEffect
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.
✔️ 1
Since
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.
👍 1
so you always saw 0, but you probably also saw ongoing CPU work on the device since it was still generating frames in that
withFrameMillis
loop
Action item for us: we should probably lint warn for a call to
mutableStateOf
inside of a composable function and suggest surrounding it with a
remember {}
.
p

prat

12/28/2020, 10:48 PM
Thanks for the detailed explanation, Adam. A lot of interesting info here
👍 1