Hi, I’m trying to create a stories ui progressbar ...
# coroutines
u
Hi, I’m trying to create a stories ui progressbar I’d best like to have that progress logic in a view model and keep the view dumb I looked into android ValueAnimator source, and its basically driven by Handler ticks What would be best to emulator this in coroutines/flow? Would this do it reliably?
Copy code
while(true) {
	yield() <--- In practise it seems yield emulates <http://Handler.post|Handler.post>
	emit(Unit)
}
Or should I just wrap the ValueAnimator as Flow and call it a day?
hm turns out handler post != frame, there can be several posts in one frame
a
there's a few interesting elements to this question, but the key principles are: 1. you want to keep the progress logic focused on time and not frames. The framerate could be anything, but you should always be able to compute the current state given a current time; that allows you to accommodate any framerate, or framerates that change for some reason. 2. Frame timing is usually a function of the display the animation is running on and related to its refresh rate. Your ViewModel is not a source of frame timing.
Since you're mentioning ValueAnimator I'm assuming you're working on Android, where the source of truth for frame timing is the
Choreographer
. The way we work with this in Compose is exposed through a
MonotonicFrameClock
CoroutineContext
element that is used by the
withFrameNanos
API; the implementation we use for Compose UI has its entry point here: https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:compose/[…]n/androidx/compose/ui/platform/AndroidUiFrameClock.android.kt
in short,
suspendCancellableCoroutine
is used to run frame logic as part of the Choreographer frame callback and then the coroutine is resumed. The
withFrameNanos
API accepts a lambda parameter of the code to run on the frame before layout and drawing run; this allows animations to affect the current frame rather than not run until the frame is complete. Android's choreographer runs frame callbacks, layout, and drawing atomically before any other dispatched coroutines can run. Coordinating this is the reason for the
AndroidUiDispatcher
to exist.
If you're using compose the easiest thing you can do is to use the
withFrameNanos
API from a coroutine launched as part of your composition, i.e. from
LaunchedEffect
or a scope obtained from
rememberCoroutineScope()
. You might do this from a
suspend fun
in your
ViewModel
like this:
Copy code
suspend fun updateFrameProgress() {
  withFrameNanos { frameTime ->
    updateProgress(frameTime)
  }
}
and then call it from a
LaunchedEffect
:
Copy code
LaunchedEffect(myViewModel) {
  while (true) { // will cancel as needed
    myViewModel.updateFrameProgress()
  }
}
None of this is magic though, it's all based on standard coroutines
u
Thank you for such elaboration, so the tldr; of it is, my progress is driven by framerate, which is exposed as low level OS api, so since I’m not doing using Compose yet, I should wrap Choreographer to a Flow, or rather ValueAnimator to get all the extra goodies Correct?
Also, how would you model this in a real app? I was thinking of emitting from a ViewModel something like
data class Progress(val pageIndex: Int, val pageFraction)
where the fraction is normalized to 0F..1F Would that not be a lot of garbage for your taste?
a
measure it before you worry about it
a version of
withFrameNanos
described above for Android without Compose can be as simple as:
Copy code
suspend inline fun <R> withFrameNanos(crossinline block: (Long) -> R): R =
    suspendCancellableCoroutine { continuation ->
        val frameCallback = Choreographer.FrameCallback { frameTimeNanos -> 
            continuation.resumeWith(runCatching { block(frameTimeNanos) })
        }
        val choreographer = Choreographer.getInstance()
        choreographer.postFrameCallback(frameCallback)
        continuation.invokeOnCancellation { choreographer.removeFrameCallback(frameCallback) }
    }
today on android there's one choreographer linked to the primary display of the device, but there's been discussion over the past few years around changing that and scoping it to the display/window/surface/context; don't assume you'll always be able to animate without a view or context to provide a scope
one of the most common performance bugs we've seen over the years linked to animations isn't around churning a lot of garbage, it's about bugs in animation scope management that keep animations running even when the app isn't visible
u
not sure how would I measure it, in terms of garbage, i.e. what is acceptable or not for example its 6s story, so emit every 11ms on a Pixel 5, so 545 objects
yea I have that in mind
a
look at some traces from a release build of your app using https://ui.perfetto.dev
you can see how much time you're taking each frame
u
okay so my measure is to not blow the frame time budget?
a
garbage collection can be hard to measure the impact of, since many recent versions of android use a concurrent collector that runs gc from a background thread, but while it's running, object field reads are slower due to read barriers
but if you see a gc thread running a lot, it might be time to see where it's coming from. Chances are one animation like this isn't going to be the root cause of major issues; you're talking about one per progress indicator per frame.
the basic approach outlined above is what we do in compose for animations and it's generally not the state of the animation itself that leads to the bulk of the associated work, it's what code downstream of that does in response to the animation state
u
I see, I was also wondering about that with ValueAnimator,, the api is
Copy code
animator.addUpdateListener {
    val value = it.animatedValue as Float
}

public Object ValueAnimator.getAnimatedValue()
sooo it appears that if I
ValueAnimator(0F, 1F)
then the
animatedValue
is boxed anyways, right?
a
I would need to look back at the code for animators, it's been years. But I recall there was a variant that worked in raw primitives instead of boxed values for cases of measured differences. Keep in mind that ValueAnimator and friends are from API level 11, written in 2010 for release in 2011 and the associated hardware and dalvik runtime of that time period
these things can still matter at a certain point of scale in your app, but it's usually more important to keep in mind where the high bit in the measurement comes from
u
yea but the snippet above I used ever since 2010 😄 and it seems now that it boxes and perf never bothered me with it
a
then probably don't worry about it 🙂
u
yes, thank so you much!
👍 1