A dive into Async-Await on Android
Note: this article was written for a preview version of coroutines for Kotlin. Its details have changed since then.
In a previous article I provided a glimpse into the world of async-await on Android. Now it’s time to dive a little bit deeper in this upcoming functionality in Kotlin 1.1
What is async-await for?
When dealing with long-running operations like network calls or database transactions, you need to make sure you schedule this work to a background thread. If you forget to do this, you may end up with blocking the UI thread until the task is finished. During that time, the user cannot interact with your application.
Unfortunately when you schedule a new task in the background, you cannot use its result directly. Instead, you’re gonna have to use some sort of callback. When that callback is invoked with the result of the operation, you can continue with what you want to do, for example run another network request.
This easily flows in what people call a ‘callback hell’: multiple nested callbacks, all waiting to be invoked when some long-running task has finished.
fun retrieveIssues() {
githubApi.retrieveUser() { user ->
githubApi.repositoriesFor(user) { repositories ->
githubApi.issueFor(repositories.first()) { issues ->
handler.post {
textView.text = "You have issues!"
}
}
}
}
}
This snippet of code does three network requests, and finally posts a message to the main thread to update the text of some TextView
.
Fixing this with async-await
With async-await, you can program that same function in a more imperative way. Instead of passing a callback to the function, you can call a suspension function await
which lets you use the result of the task in a way that resembles normal synchronous code:
fun retrieveIssues() = asyncUI {
val user = await(githubApi.retrieveUser())
val repositories = await(githubApi.repositoriesFor(user))
val issues = await(githubApi.issueFor(repositories.first()))
textView.text = "You have issues!"
}
This snippet of code still does three network requests and updates a TextView
on the main thread, and still does not block the UI!
Wait.. what?!
If you use AsyncAwait-Android, you’re given a couple of functions. Two of them are async
and await
.
The async
function enables the use of await
and changes the way method results are handled. When entering the function, each line of code is executed synchronously until a suspension point is reached. In this case, this is a call to await
. That is all async
does! It does not move any code to a background thread.
The await
function enables things to get asynchronous. It receives an ‘awaitable’ as a parameter, where ‘awaitable’ is some asynchronous operation. When await
is called, it registers with the awaitable to be notified when the operation has finished, and returns from the asyncUI
method. When the awaitable has completed, it will execute the remainder of the method, passing the resulting value to it.
The magic
This all seems magic, but there’s no real magic involved. Instead, the Kotlin compiler transforms the coroutine (that’s what the function passed to async
is called) into a state machine. Each state represents a piece of code from the coroutine. A suspension point (the call to await
) denotes the end of a state. When an awaited task has finished, the next state is invoked, and so on.
If we take a simpler version of our code snippet before, we can see what states are created. Remember that each call to await
denotes a suspension point:
fun retrieveIssues() = async {
println("Retrieving user") val user = await(githubApi.retrieveUser())
println("$user retrieved") val repositories = await(githubApi.repositoriesFor(user))
println("${repositories.size} repositories")
}
For this coroutines there are three states:
- The initial state, before any suspension point
- After the first suspension point (
await(githubApi.retrieveUser())
) - After the second suspension point (
await(githubApi.repo...)
)
This code is compiled to the following state machine (pseudo-byte code):
class <anonymous_for_state_machine> {
// The current state of the machine
int label = 0
// Local variables for the coroutine
User user = null
List<Repository> repositories = null
void resume (Object data) {
if (label == 0) goto L0
if (label == 1) goto L1
if (label == 2) goto L2
L0:
println("Retrieving user")
// Prepare for await call
label = 1
await(githubApi.retrieveUser(), this)
// 'this' is passed as a continuation
return
L1:
user = (User) data
println("$user retrieved")
label = 2
await(githubApi.repositoriesFor(user), this)
return
L2:
repositories = (List<Repository>) data
println("${repositories.size} repositories")
label = -1
return
}
}
When entering the state machine, label == 0
and the first block of code is executed. When an await
is reached, the label
is updated, and the state machine is passed to the await call. Execution returns from the resume
method at this point.
When the task passed to await
has finished, await
invokes resume(data)
on the state machine, and the next piece of code is executed. This is continued until the last state is reached.
Exception handling
If an awaitable terminates with an exception, the state machine is notified of that exception. In fact, the resume
method actually takes in an extra Throwable
parameter. Each time a new state is executed, it first checks if the Throwable
isn’t null. If it isn’t, it is thrown.
This way, you can use a regular try / catch
clause in your coroutine:
fun foo() = async {
try {
await(doSomething())
await(doSomethingThatThrows())
} catch(t: Throwable) {
t.printStackTrace()
}
}
Threading
await
does not ensure that the awaitable is ran on a background thread. Instead, it merely registers a listener to the awaitable to be notified when finished. It is the task of the awaitable to make sure computation happens on a proper thread.
For example, you may pass a retrofit.Call<T>
to await
. At that point, enqueue()
is invoked on the parameter and a callback is registered. Retrofit makes sure that the network call is made on a background thread:
suspend fun <R> await(
call: Call<R>,
machine: Continuation<Response<R>>
) {
call.enqueue(
{ response ->
machine.resume(response)
},
{ throwable ->
machine.resumeWithException(throwable)
}
)
}
For convenience, there is one version of await
that does move its task to a background thread. This takes in a function () -> R
, which is scheduled on a background thread:
fun foo() = async<String> {
await { "Hello, world!" }
}
async, async<T> and asyncUI
There are three flavors of async
:
async
: does not return anything (likeUnit
orvoid
)async<T>
: returns a value of typeT
asyncUI
: does not return anything.
When using async<T>
, you need to return a value of type T
in the coroutine. async<T>
itself returns a Task<T>
which itself is, as you might have guessed, an awaitable. This way you can await on other async functions:
fun foo() = async {
val text = await(bar())
println(text)
}fun bar() = async<String> {
"Hello world!"
}
Furthermore, asyncUI
ensures that the continuation (e.g. the next state) is called on the main thread. If you use async
or async<T>
, the continuation will be called on the thread at which the callback was called:
fun foo() = async {
// Runs on calling thread
await(someIoTask()) // someIoTask() runs on an io thread
// Continues on the io thread
}fun bar() = asyncUI {
// Runs on main thread
await(someIoTask()) // someIoTask() runs on an io thread
// Continues on the main thread
}
Wrapping up
As you can see, coroutines provide great possibilities and may increase readability of your code if done right. Coroutines are currently available in Kotlin 1.1-M02, and the async-await funtionality described in this article can be found in my library on Github.
This article is inspired by Async and Await by Stephen Cleary and the informal design description on coroutines. I highly recommend reading these if you want to know more. If you think you’ve read enough for the day, you can also watch this talk by Andrey Breslav.