Simple asynchronous loading with Kotlin Coroutines
One of the most challenging things in software development is anything that is asynchronous. Unfortunately, humans are really bad at…
One of the most challenging things in software development is anything that is asynchronous. Unfortunately, humans are really bad at multi-tasking. Just imagine yourself trying to do more than one thing at the time, especially when both of these things require you to think actively about it.
Computers, on the other hand, are really good at multi-tasking. Writing good software usually requires us (developers) to have a good understanding of multi-tasking and doing things asynchronously. On Android, this includes things like the asynchronous lifecycle callbacks of activities and fragments, and dealing with all the background tasks that we trigger. Since we’re so bad at multi-tasking, even thinking about it when we write our code is hard.
Kotlin Coroutines is the latest addition to the toolbox of asynchronous APIs and libraries. It is not a silver bullet that solves all your problems, but hopefully it will make things a bit easier in many situations. I’m not going to try to explain the inner working of coroutines in this post, but instead show a simple example how to use it in Android development.
Let’s get started!
Gradle stuff
Kotlin Coroutines requires us to add some more stuff to our application modules build.gradle file. We start by adding the following right after the android section;
kotlin {
experimental {
coroutines 'enable'
}
}
Next, we add the following two dependencies;
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.20"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.20"
Your first coroutine
Our task; we want to load an image from the media storage and later display it in an ImageView. The synchronous function for loading it looks like this;
fun loadBitmapFromMediaStore(imageId: Int,
imagesBaseUri: Uri): Bitmap {
val uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
return MediaStore.Images.Media.getBitmap(contentResolver, uri)
}
This function must be called on a background thread in our application since it is an IO operation. This means we would have to use one of the many solutions for launching this in the background. However, once the function returns a bitmap, we want to display it;
imageView.setImageBitmap(bitmap)
This call must run on the main thread of the application, or it will crash. Considering that we basically only have three lines of code here, we would like to be able to write it like this;
val uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
imageView.setImageBitmap(bitmap)
The code above will either cause the application to freeze momentarily (bad UX!) or crash, depending on which thread it is running on or how long it takes to load the bitmap. Let’s see how this would look like if we used Kotlin Coroutines:
val job = launch(Background) {
val uri = Uri.withAppendedPath(imagesBaseUri, imageId.toString())
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver,
launch(UI) {
imageView.setImageBitmap(bitmap)
}
}
For now, we can ignore the return value job. We’ll get back to that in a moment. The important thing here is the function launch and the two parameters Background and UI. Note that the only difference from the previous three lines are the calls to launch(). We can easily follow the code and it is almost identical to the completely synchronous example shown earlier.
What launch() does is creating and starting a coroutine. The Background parameter is a CoroutineContext that makes sure that the coroutine runs on a background thread, which makes sure your application won’t stall or crash. You can declare such a CoroutineContext like this;
internal val Background = newFixedThreadPoolContext(2, "bg")
This will create a new context that coroutines use which we’re calling “bg” and it will use two regular threads when executing its stuff.
Inside our first coroutine (the call to launch(Background)) we make a call to launch(UI). This will trigger another coroutine, this time running on the pre-defined context that uses the Android main thread. This means that our call to imageView.setImageBitmap() will run safely on the main thread without crashing the application.
Cancellation
The code above is probably nothing you haven’t done before using some other APIs. The first challenge comes when dealing with the activity lifecycle. If the user leaves the activity before the loading is completed, our application will cause a crash once it tries to call imageView.setImageBitmap(). To deal with this, we have to cancel the loading to make sure this call never happens. This is where the return value from launch() is important. We take the job variable shown above and store it until the activity reaches onStop(), and then we simply do the following;
job.cancel()
This is the same thing you have to do if we were to use RxJava (call dispose() on your Disposable) or AsyncTask (call cancel() on the task). We haven’t gained much more than a slightly easier to read syntax for performing our background work. Let’s see if we can’t fix that.
LifecycleObserver
The Android Architecture Components is probably one of the best gifts from Google to Android developers since the introduction of the support libraries. There have been tons of great stuff written about ViewModel, Room and LiveData. Another really great part is the Lifecycle API. This gives us a convenient way for safely listening to lifecycle changes in an activity or fragment and react accordingly. We define the following to be used with our coroutines;
class CoroutineLifecycleListener(val deferred: Deferred<*>) : LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
fun cancelCoroutine() {
if (!deferred.isCancelled) {
deferred.cancel()
}
}
}
We also create an extension function on LifecycleOwner (implemented by FragmentActivity and the support Fragment) like this;
fun <T> LifecycleOwner.load(loader: () -> T): Deferred<T> {
val deferred = async(context = Background,
start = CoroutineStart.LAZY) {
loader()
}
lifecycle.addObserver(CoroutineLifecycleListener(deferred))
return deferred
}
Ok, that’s a lot of new stuff in one function. Let’s take it one piece at a time.
This is simply a regular function added as an extension on all classes implementing LifecycleOwner, which means we can call it from an activity or fragment when we use the support library (which we all should do!). Now we can call load() inside a fragment or activity, and from within that function access the lifecycle member and add our CoroutineLifecycleListener as an observer.
The load() function takes a lambda named loader as a parameter, which returns a generic type T. Inside the load() function, we call another coroutine creator named async(), which will run in the background using the Background coroutine context. Note that this call has a second parameter: start = CoroutineStart.LAZY. This means that this coroutine won’t start until someone explicitly asks it for a return value. You’ll soon see how to do this.
The coroutine then returns a Deferred<T> object to the caller. This is a similar object as the job we had earlier, but it can also carry a deferred value, like a JavaScript Promise or a Future<T> in the regular Java APIs. The nice thing is that it has an await() method that works in coroutines, as we will soon seen.
Next, we define another extension function named then(), this time on the class Deferred<T>, which is the type we return from our load() function above. It also take a lambda as a parameter, named block, which takes a single object of type T as its parameter.
infix fun <T> Deferred<T>.then(block: (T) -> Unit): Job {
return launch(context = UI) {
block(this@then.await())
}
}
This function will create another coroutine using the launch() function, this time running on the main thread. The lambda (named block) passed to this coroutine takes the value from the completed Deferred object as its parameter. We make a call to await() which will suspend the execution of this coroutine until that Deferred object returns a value.
Here is where coroutines become so impressive. The call to await() is done on the main thread, but it doesn’t block further execution on that thread. It will simply suspend the execution of that function until it is ready, when it will resume and pass the value from the Deferred to the lambda. While the coroutine is suspended, the main thread can keep executing other things. Suspending functions is a central concept within coroutines and what creates the magic about the whole thing.
The lifecycle observer added in the load() function will cancel the first coroutine once onDestroy() is called on our activity. This will also cause the second coroutine to get cancelled and prevent block() from being called.
Kotlin Coroutine DSL
Now we got two extension functions and a class that will take care of the cancellation of the coroutine. Let’s see how we can use this;
load {
loadBitmapFromMediaStore(imageId, imagesBaseUri)
} then {
imageView.setImageBitmap(it)
}
In the code above we pass a lambda to the load() function that makes a call to loadBitmapFromMediaStore(), which must run on a background thread. Since that function returns a Bitmap, the return value from load() will be Deferred<Bitmap>.
As the extension function then() was declared with infix we can perform the fancy syntax shown above on the returned Deferred<Bitmap>. The lambda we pass to our then() function will receive a Bitmap, so we can simply call imageView.setImageBitmap(it). Cancellation is also taken care of thanks to the lifecycle observer.
The code above can be used for any asynchronous call that needs to happen on a background thread and where the return value should be passed back to the main thread, like the example above. It isn’t as powerful as using something like RxJava where you can compose multiple calls, but it is much easier to read and will probably cover a lot of the most common cases. Now you can safely do stuff like this, without having to worry about leaking Context or deal with threads in every call;
load { restApi.fetchData(query) } then { adapter.display(it) }
The code for load() and then() is too small to motivate a new library, but I do expect something similar to appear in a future Kotlin-based library for Android, once coroutines reach a stable release.
Until then, you can either use or adapt the code above or have a look at Anko Coroutines. I have also published a more complete version of this on GitHub. Have fun with your adventures in the land of coroutines!