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.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

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 Deferreds. But you want the results when they become available. You’ll have to use await(), a suspending function, on the Deferreds, 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 the async block, through await(), 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 from await(), use a try/catch block either on the await() call, or within async().

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.