Android views 💘 callbacks

The Android view system loves callbacks; like really loves callbacks. To give you an idea, there are currently 80+ callbacks in view and widgets classes in the Android framework, and then another 200+ in Jetpack (includes non-UI libraries, but you get the idea).

Commonly used examples include:

AnimatorListener to know when an animator finishes.

to know when an animator finishes. RecyclerView.OnScrollListener to know when the scroll state changes.

to know when the scroll state changes. View.OnLayoutChangeListener to know when a view is laid out.

Then there are the APIs which accept a Runnable to perform an async action, such as View.post() or View.postDelayed() , etc.

There are so many callbacks because user interface programming on Android is inherently asynchronous. Everything from measure & layout, drawing, to inset dispatch are all performed asynchronously. Generally, something (usually a view) requests a traversal from the system, and then some time later the system dispatches the call, which then triggers any listeners.

KTX extension functions

For a lot of the APIs we’ve mentioned above, the team has added extension functions in Jetpack to improve the developer ergonomics. One of my favorites is View.doOnPreDraw() , which greatly simplifies waiting for the next draw to happen. There are many others which I use every day: View.doOnLayout() and Animator.doOnEnd() to name two.

But these extension functions only go so far: they make a old-school callback API into a Kotlin-friendly lambda-based API. They’re nicer to use but we’re still dealing with callbacks in a different form, which makes performing complex UI operations more difficult. Since we’re talking about asynchronous operations, could we could benefit from coroutines here? 🤔

Coroutines to the rescue

This blog post assumes a working level of coroutines knowledge. If something sounds alien to you below, we published a blog post series earlier this year to help you recap:

Suspending functions are one of the basic units of coroutines, allowing us to write code in non-blocking way. This is important when we’re dealing with Android UI, since we never want to block the main thread, which can result in performance problems like jank.

suspendCancellableCoroutine

In the Kotlin coroutines library there are a number of coroutine builder functions which enable wrapping callback based APIs with suspending functions. The primary API is suspendCoroutine() , with a cancellable version called suspendCancellableCoroutine() .

We recommend to always use suspendCancellableCoroutine() since it allows us to handle cancellation in both directions:

#1: The coroutine can be cancelled while the async operation is pending. Depending on the scope the coroutine is running in, the coroutine might be cancelled if the view is removed from the view hierarchy. Example: fragment is popped off the stack. Handling this direction allows us to cancel any async operations, and clean up any ongoing resources.

#2: The async UI operation is cancelled (or throws an error) while the coroutine is suspended. Not all operations have a cancelled or error state but for those that do, like Animator below, we should propagate those states to the coroutine, allowing the caller of the method to handle the error.

Wait for a view to be laid out

Let’s take a look at an example which wraps up the task of waiting for the next layout pass on a view (e.g. you’ve changed the text of a TextView and need to wait for a layout pass to know it’s new size):

This function only supports cancellation in one direction, from the coroutine to the operation (#1), since layout does not have an error state we can observe.

We can then use it like so:

We’ve just built an await function for a View’s layout. The same recipe can be applied to many commonly used callbacks, such as doOnPreDraw() to know when a draw pass is about to happen, postOnAnimation() to know when the next animation frame is, and so on.

Scope

You’ll notice in the example above that we’re using a lifecycleScope to launch our coroutine. What is that?

The scope which we use to run any coroutines is especially important when we’re touching the UI, to avoid accidentally leaking memory. Luckily there are a number Lifecycle s available which are appropriately scoped for our views. We can then use the lifecycleScope extension property to obtain a CoroutineScope which is scoped to that lifecycle.

LifecycleScope is available in the AndroidX lifecycle-runtime-ktx library. You can find more information here.

A commonly used lifecycle owner is Fragment ’s viewLifecycleOwner , which is active for as long as the fragment’s view is attached. Once the fragment’s view is removed, the attached lifecycleScope is automatically cancelled. And because we’re adding cancellation support to our suspending functions, everything will be automatically cleaned-up if this happened.

Waiting for an Animator to finish

Let’s look at another example, this time awaiting an Animator to finish:

This function supports cancellation in both directions, as both the Animator and the coroutine can be separately cancelled.

#1: The coroutine is cancelled while the animator is running. We can use the invokeOnCancellation callback to know when the coroutine has been cancelled, enabling us to cancel the animator too.

#2: The animator is cancelled while the coroutine is suspended. We can use the onAnimationCancel() callback to know when the animator is cancelled, allowing us to call cancel() on the continuation, to cancel the suspended coroutine.

We have just learnt the basics of wrapping up callback API into a suspending await function. 🏅

Orchestrating the band

At this point you might be thinking “great, but what does this give me?” In isolation these functions don’t do a lot, but when you start combining them together they become really powerful.

Here’s an example which uses Animator.awaitEnd() to run 3 animators in sequence:

For this particular example, you could instead put them all into a AnimatorSet , and get the same effect.

But this technique works for different types of async operations; here using a ValueAnimator , a RecyclerView smooth scroll, and an Animator :

Try doing this with an AnimatorSet 🤯. To achieve this without coroutines would mean adding listeners to each operation, which would start the next operation, and so on. Yuck.

By modeling different asynchronous operations as suspend functions, we gain the ability to orchestrate them expressively and concisely.

We can go even further though…