Kotlin Coroutines Tutorial for Android: Getting Started
In this Kotlin Coroutines tutorial, you’ll learn how to write asynchronous code just as naturally as your normal, synchronous code. By Amanjeet Singh.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Kotlin Coroutines Tutorial for Android: Getting Started
30 mins
- Why Use Coroutines?
- Getting Started
- Introduction to Coroutines
- Suspending vs. blocking
- Coroutine Terminology
- Adding Kotlin Coroutines support
- Setting Up Your Code
- Coroutine Jobs
- Using Dispatchers With Kotlin Coroutines
- Scoping Kotlin Coroutines
- Downloading Images With Kotlin Coroutines
- Applying the Snow Filter
- Putting Everything Together
- Resolving the Error
- Internal Workings of Coroutines
- Handling Exceptions
- Cleaning Up
- Writing Expressive Kotlin Coroutines
- Where to Go From Here?
Applying the Snow Filter
You need another function which uses coroutines to apply a filter. Create a function loadSnowFilterAsync()
as follows, at the bottom of TutorialFragment.kt:
private fun loadSnowFilterAsync(originalBitmap: Bitmap): Deferred<Bitmap> =
coroutineScope.async(Dispatchers.Default) {
SnowFilter.applySnowEffect(originalBitmap)
}
Applying a filter is a heavy task because it has to work pixel-by-pixel, for the entire image. This is usually CPU intensive work, so you can use the Default dispatcher to use a worker thread.
You create a function, loadSnowFilterAsync>()
, which takes a bitmap as an argument. This method returns a Deferred
again. It represents a Bitmap
with a snowy filter applied to it. It uses the async()
builder to execute on a worker thread and apply the snow filter on the Bitmap
.
Putting Everything Together
At this point, you’ve created all the necessary code, so you’re ready to put all the pieces together and create some Kotlin Coroutines. You’ll see how they make creating asynchronous operations easy.
Right now, you’re returning Deferred
s. But you want the results when they become available. You’ll have to use await()
, a suspending function, on the Deferred
s, which will give you the result when it’s available. In your case – a Bitmap
.
For example, to get your original bitmap, you’ll call getOriginalBitmapAsync(tutorial).await()
. However, calling this function directly from onViewCreated()
of the TutorialFragment will give you the following error:
There are two things to note here:
- await is a suspending function; calling this function from a non-suspended instance would give an error. You can only call this function from within a coroutine or another suspended function. Just like the error states.
-
Android Studio is capable of showing you such suspension points in your code. For example, calling
await()
would add a suspension point, displaying a green arrow next to your line number.
Suspension points are markings the compiler creates, to let the system and the user know that a coroutine could be suspended there.
Resolving the Error
To get rid of this error, you’ll need to await()
in a coroutine. Then, you’ll have to update the UI thread as soon as you apply the snow filter. To do this, use a coroutine builder like so:
coroutineScope.launch(Dispatchers.Main) {
val originalBitmap = getOriginalBitmapAsync(tutorial).await()
}
In this code snippet, you launch a coroutine on the main thread. But the originalBitmap
is computed in a worker thread pool, so it doesn’t freeze the UI. Once you call await()
, it will suspend launch()
, until the image value is returned.
Now, you need to call a method to apply the snow filter on the image, then load the final image.
To do it, create loadImage()
, underneath this code, as follows:
private fun loadImage(snowFilterBitmap: Bitmap){
progressBar.visibility = View.GONE
snowFilterImage?.setImageBitmap(snowFilterBitmap)
}
By calling up loadSnowFilterAsync()
to get the filtered Bitmap
and loading it into the Image view, you’ll get:
coroutineScope.launch(Dispatchers.Main) {
val originalBitmap = getOriginalBitmapAsync(tutorial).await()
//1
val snowFilterBitmap = loadSnowFilterAsync(originalBitmap).await()
//2
loadImage(snowFilterBitmap)
}
You’re simply applying the filter to a loaded image, and then passing it to loadImage()
. That’s the beauty of coroutines: they help convert your async operations into natural, sequential, method calls.
Internal Workings of Coroutines
Internally, Kotlin Coroutines use the concept of Continuation-Passing Style programming, also known as CPS. This style of programming involves passing the control flow of the program as an argument to functions. This argument, in Kotlin Coroutines’ world, is known as Continuation.
A continuation is nothing more than a callback. Although, it’s much more system-level than you standard callbacks. The system uses them to know when a suspended function should continue or return a value.
For example, when you call await()
, the system suspends the outer coroutine until there is a value present. Once the value is there, it uses the continuation, to return it back to the outer coroutine. This way, it doesn’t have to block threads, it can just notify itself that a coroutine needs a thread to continue its work. Really neat, huh? :]
Hopefully, it now makes a bit more sense, and it doesn’t seem all that magical! Another really important thing to understand, in Kotlin Coroutines, is the way exceptions are handled.
Handling Exceptions
Exception handling in Kotlin Coroutines behaves differently depending on the CoroutineBuilder you are using. The exception may get propagated automatically or it may get deferred till the consumer consumes the result.
Here’s how exceptions behave for the builders you used in your code and how to handle them:
-
launch: The exception propagates to the parent and will fail your coroutine parent-child hierarchy. This will throw an exception in the coroutine thread immediately. You can avoid these exceptions with
try/catch
blocks, or a custom exception handler. -
async: You defer exceptions until you consume the result for the
async
block. That means if you forgot or did not consume the result of theasync
block, throughawait()
, you may not get an exception at all! The coroutine will bury it, and your app will be fine. If you want to avoid exceptions fromawait()
, use atry/catch
block either on theawait()
call, or withinasync()
.
Here’s how to handle exceptions for your Snowy app:
Create a property, coroutineExceptionHandler
, as follows:
// 1
private val coroutineExceptionHandler: CoroutineExceptionHandler =
CoroutineExceptionHandler { _, throwable ->
//2
coroutineScope.launch(Dispatchers.Main) {
//3
errorMessage.visibility = View.VISIBLE
errorMessage.text = getString(R.string.error_message)
}
GlobalScope.launch { println("Caught $throwable") }
}
This creates a CoroutineExceptionHandler
to log exceptions. Additionally, it creates a coroutine on the main thread to show error messages on the UI. You also log your exceptions in a separate coroutine, which will live with your app’s lifecycle. This is useful if you need to log your exceptions to tools like Crashlytics. Since GlobalScope
won’t be destroyed with the UI, you can log exceptions in it, so you don’t lose the logs.
Now, it’s time to associate this handler with your CoroutineScope. Add the following in the property definition of coroutineScope
:
private val coroutineScope =
CoroutineScope(Dispatchers.Main + parentJob + coroutineExceptionHandler)
Now, if you have any exceptions in coroutines you start, you’ll log them and display a message in a TextView
.
Remember, if you’re using async()
, always try to call await()
from a try-catch block or a scope where you’ve installed a CoroutineExceptionHandler
.