Illustration by Virginia Poltrack

Suspending over Views

How coroutines can make UI programming easier

Chris Banes
Dec 2 · 6 min read

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:

Then there are the APIs which accept a to perform an async action, such as or , 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 , which greatly simplifies waiting for the next draw to happen. There are many others which I use every day: and 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 , with a cancellable version called .

We recommend to always use 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 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 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 to know when a draw pass is about to happen, to know when the next animation frame is, and so on.

Scope

You’ll notice in the example above that we’re using a 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 s available which are appropriately scoped for our views. We can then use the extension property to obtain a which is scoped to that lifecycle.

is available in the AndroidX library. You can find more information here.

A commonly used lifecycle owner is ’s , which is active for as long as the fragment’s view is attached. Once the fragment’s view is removed, the attached 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 and the coroutine can be separately cancelled.

#1: The coroutine is cancelled while the animator is running. We can use the 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 callback to know when the animator is cancelled, allowing us to call 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 to run 3 animators in sequence:

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

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

Try doing this with an 🤯. 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 functions, we gain the ability to orchestrate them expressively and concisely.

We can go even further though…

What if we want the and the smooth scroll to start at the same time, then start the after both have finished? Since we’re using coroutines, we can run them concurrently using :


But what if you then want the scroll to start with a delay? (similar to ). Well coroutines has you covered there too, we can use the function:


What if we then want a transition to repeat? We can wrap the whole thing with the function (or use a 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.

post.resume()

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:

Android Developers

The official Android Developers publication on Medium

Thanks to Nick Butcher

Chris Banes

Written by

Work @Google on #Android

Android Developers

The official Android Developers publication on Medium

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade