Suspending over views
Kotlin Coroutines allow us to model asynchronous problems like synchronous code. That’s great, but most usage seems to concentrate on I/O tasks and concurrent operations. Coroutines are great at modelling problems which work across threads, but can also model asynchronous problems on the same thread.
There’s one place which I think really benefits from this, and that’s working with the Android view system.
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:
AnimatorListenerto know when an animator finishes.
RecyclerView.OnScrollListenerto know when the scroll state changes.
View.OnLayoutChangeListenerto know when a view is laid out.
Then there are the APIs which accept a
Runnable to perform an async action, such as
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:
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.
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
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.
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
Lifecycles 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.
LifecycleScopeis available in the AndroidX
lifecycle-runtime-ktxlibrary. You can find more information here.
A commonly used lifecycle owner is
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.
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
suspendfunctions, we gain the ability to orchestrate them expressively and concisely.
We can go even further though…
What if we want the
ValueAnimator and the smooth scroll to start at the same time, then start the
ObjectAnimator after both have finished? Since we’re using coroutines, we can run them concurrently using
What if we then want a transition to repeat? We can wrap the whole thing with the
repeat() function (or use a
for loop). Here’s an example of a view fading in and out, 3 times:
You can even achieve neat things with the repetition count. Say you want the fade-in-fade-out to get progressively slower on each repetition:
In my mind, this is where the power of using coroutines with the Android view system really comes into its own. We can create a complex asynchronous transition, combining different animation types, without having to resort to chaining different types of callback together.
By using the same coroutine primitives which we use on the data layers of our apps, we also make UI programming more accessible. An await function is much more readable to someone new to the code base than a number of seemingly disconnected callbacks.
Hopefully this post has gotten you thinking about what other APIs could benefit from coroutines!
The follow-up post to this, which demonstrates how coroutines can be used to orchestrate a complex transition, and includes implementations for some common views, can be found here: