Learning Kotlin & Android Development

Network Fetch with Kotlin Coroutine

Handle Network Fetch, Error, Exception, and Cancellation with Kotlin Coroutine

Elye
Elye
Jan 3 · 9 min read

About 3 years ago, my prediction that Kotlin Coroutine would be the way Network Fetching be. I provided some simple examples in the article.

However, there’s nothing on how we can handle the Error, Exception, and Cancellation. Here in this article, I’m providing the barebone Kotlin Coroutine way of network fetching (without Retrofit, RxJava, or Kotlin-Flow), so we have a good grasp of how it works.

The Example App.

Accompanied with the article, I also provide the example add with code below. The network fetch is doing a keyword fetch for the count of its occurrence in Wikipedia. Check here for detail.

I use both approach of launch and async-await approach of fetching.

Image for post
Image for post

Recap the basic Coroutine Network Fetching.

Below are the 2 fetching approaches

Launch

In this fetching, we will launch the fetching in another thread. It is like fire-and-forget, and let it handle its own return when it is done.

fun fetchData(searchText: String) {
coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope?.launch {
Log.d("Track", "Launch Fetch Started")
val result = Network.fetchHttpResult(searchText)
launch(Dispatchers.Main) {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
Log.d("Track", "Launch Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
Log.d("Track", "Launch Post Success Result")
}
}
}
}
}
Image for post
Image for post

From the code above, we can see that it first launch a coroutine in a new thread (Dispatchers.IO), to perform a fetch.

After the fetch is completed, it then launch a launch a new coroutine in the main thread (Dispatchers.Main), to update the UI with the result.

What’s nice about coroutine is, although the code looks sequential, it is done asynchronously.

Async-await

In this fetching, we will launch a coroutine in the main thread, which will launch the fetch in another thread, and wait for it to finish.

fun fetchData(searchText: String) {
coroutineScope = MainScope()
coroutineScope?.launch {
val defer = async(Dispatchers.IO) {
Log.d("Track", "Async Fetch Started")
Network.fetchHttpResult(searchText).apply {
Log.d("Track", "Async Fetch Done")
}
}
when (val result = defer.await()) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Success Result")
}
}
}
}
Image for post
Image for post

From the code above, we can see that it first launch a coroutine in the main thread (Dispatchers.MAIN), to launch async in a new thread (Dispatchers.IO) to perform the fetch.

While it is fetching, the await is called to wait for the fetching to finish. Since this is a coroutine, the function is suspended for the original main thread to continue what is it doing, hence not blocking the main thread from the UI work as shown in the diagram above. (white block within the Main Thread Coroutine block)

Coroutine Exception Handling

One of the seeming drawback of Coroutine is error handling compared to RxJava is, it doesn’t seem as clean within the flow.

Try-Catch Flow (not ideal way)

To use a try-catch, it will look something as below

// Launch approach (NOT-IDEAL)
fun fetchData(searchText: String) {
coroutineScope = CoroutineScope(Dispatchers.IO)
coroutineScope?.launch {
try {
val result = Network.fetchHttpResult(searchText)
launch(Dispatchers.Main) {
try {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
}
}
} catch (e: Exception) {
// Main Thread Coroutine catch
}
}
} catch (e: Exception) {
// IO Thread Coroutine catch
}
}
}

and below

// Asycn-Await approach (NOT-IDEAL)
fun fetchData(searchText: String) {
coroutineScope = MainScope()
coroutineScope?.launch {
try {
val defer = async(Dispatchers.IO) {
try {
Log.d("Track", "Async Fetch Started")
Network.fetchHttpResult(searchText).apply {
Log.d("Track", "Async Fetch Done")
}
} catch (e: Exception) {
// IO Thread Coroutine catch
}
}
when (val result = defer.await()) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Success Result")
}
}
} catch (e: Exception) {
// Main Thread Coroutine catch
}
}
}

Each of the coroutine, we’ll need to have try-catch. Messy.

To do this better, fortunately, we have CoroutineExceptionHandler.

Coroutine Exception Handler (better way)

To avoid having to do multiple try-catch, we can use CoroutineExcpetionHandler.

val errorHandler = CoroutineExceptionHandler { context, error ->
coroutineScope?.launch(Dispatchers.Main) {
view.updateScreen(error.localizedMessage ?: "")
}
}

This handler will be triggered when an exception is caught in the coroutine (or the child coroutine).

The thread it runs in is the same thread (e.g. XX Thread as per diagram below) that the scope that the handler is assigned into. In order to ensure that the update is on the UI thread only, we just launch it to Dispatchers.Main in our case.

Image for post
Image for post
The XX Thread can be IO Thread or Main Thread or any other thread

Register the CoroutineExcpetionHandler

To register the error handler, just use it as part of CoroutineContext and pass it through the top level launch as shown below

// Launch approach
fun fetchData(searchText: String) {
coroutineScope = CoroutineScope(SupervisorJob()+Dispatchers.IO)
coroutineScope?.launch(errorHandler) {
Log.d("Track", "Launch Fetch Started")
val result = Network.fetchHttpResult(searchText)
launch(Dispatchers.Main) {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
Log.d("Track", "Launch Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
Log.d("Track", "Launch Post Success Result")
}
}
}
}
}
// Async-await approach
fun fetchData(searchText: String) {
coroutineScope = MainScope()
coroutineScope?.launch(errorHandler) {
val defer = async(Dispatchers.IO) {
Log.d("Track", "Async Fetch Started")
Network.fetchHttpResult(searchText).apply {
Log.d("Track", "Async Fetch Done")
}
}
when (val result = defer.await()) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
Log.d("Track", "Async Post Success Result")
}
}
}
}

Do note that it is using SupervisorJob() within the coroutine scope.

CoroutineScope(SupervisorJob() + Dispatchers.IO)

This is to ensure that the exception faced in the child coroutine, will not stop the parent coroutine from continuing. Without the SupervisorJob, it will not be able to launch in the CoroutineExceptionHandler.

For MainScope(), it is already

CoroutineScope(SupervisorJob() + Dispatchers.Main)

For more info, check out the below links

Cancelling Network Call

To cancel the coroutine, one can get a job.cancel() or scope.cancel(). In our case, we want to cancel all the jobs within the scope, we’ll use scope.cancel().

To detect cancelation is done, we can catch the CancellationException just to report that cancelation is done. Note that this Exception will not be sent to CoroutineExceptionHandler.

coroutineScope?.launch(errorHandler) {
try {
// Coroutine running
} catch (e: CancellationException) {
// Cancellation detected
}
}

Unlike some other framework, the Coroutine way of canceling is cooperative. If a coroutine run without any suspension function call e.g. yield, delay, etc, trigger to cancel it will not stop it from running.

Refer to the below for an understanding of how Coroutine works.

This then raises the question, if a network call is in progress, and we cancel the coroutine, what happens? Will it get canceled? Should we add yield in it?

Launch Approach

Let’s look into the Launch Approach.

We can try the code below, you’ll realize the cancelation won’t be caught

// Cancellation won't be caught
coroutineScope?.launch(errorHandler) {
try
{
logOut("Launch Fetch Started")
val result = Network.fetchHttpResult(searchText)
logOut("Launch Fetch Done")
launch(Dispatchers.Main) {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
logOut("Launch Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
logOut("Launch Post Success Result")
}
}
}
} catch (e: CancellationException) {
logOut("Launch Cancel Result")
}
}

The log output we’ll see is

Launch Fetch Started
Launch Fetch Done

It won’t call launch(Dispatchers.Main), as in the launch function, it will check for cancellation before proceeding. Given the cancellation has been triggered, it will not proceed.

However, in our case, we also won’t get the CancellatinException. If one code doesn’t need to detect cancellation, this is fine, one can remove the try-catch as it is not helping.

In our case, as we want to catch the CancellationException, we can add yield() function to check for the cancellation before proceed to launch(Dispatchers.Main). If cancellation is detected in the yield() suspend function, it will then trigger the CancellationException.

Hence the below code will do the trick.

coroutineScope?.launch(errorHandler) {
try
{
logOut("Launch Fetch Started")
val result = Network.fetchHttpResult(searchText)
logOut("Launch Fetch Done")
yield()
launch(Dispatchers.Main) {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
logOut("Launch Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
logOut("Launch Post Success Result")
}
}
}
} catch (e: CancellationException) {
logOut("Launch Cancel Result")
}
}

Note the output log will then be (assuming we cancel the process during fetching)

Launch Fetch Started
Launch Fetch Done
Launch Cancel Result

Note the cancelation check only comes after Launch Fetch Done, because the cancelation check is not there until then.

Image for post
Image for post

Optional launch approach

Other than using yield(), we can use isActive flag within the coroutine.

coroutineScope?.launch(errorHandler)
logOut("Launch Fetch Started")
val result = Network.fetchHttpResult(searchText)
logOut("Launch Fetch Done")

if (!isActive) { logOut("Launch Cancel Result") }
launch(Dispatchers.Main) {
when(result) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
logOut("Launch Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
logOut("Launch Post Success Result")
}
}
}
}

This will work too. We no longer need to catch the CancellationException, unless we want to throw that ourselves.

We also don’t need to do as below, since launch will also check for the active status.

if (!isActive) { 
logOut("Launch Cancel Result")
} else {
launch(Dispatchers.Main) { ... }
}

To get the complete status of Job Scope, refer here.

From above, we know that the launch approach is not ideal for cancellation detection, since cancel won’t be detected until the fetching job is completed.

Let’s look at the async-await approach.

Await-async Approach

Here, we just use a normal try-catch approach to capture the CancellationException.

coroutineScope?.launch(errorHandler) {
try {

val defer = async(Dispatchers.IO) {
logOut("Async Fetch Started")

Network.fetchHttpResult(searchText).apply {
logOut("Async Fetch Done")
}
}
when (val result = defer.await()) {
is Network.Result.NetworkError -> {
view.updateScreen(result.message)
logOut("Async Post Error Result")
}
is Network.Result.NetworkResult -> {
view.updateScreen(result.message)
logOut("Async Post Success Result")
}
}
} catch (e: CancellationException) {
logOut("Async Cancel Result")
}

}

This works. Cancelation is detected immediately. It is caught by the await suspend function that is waiting for the spawn job to complete, while continuing to check for cancellation.

The log as below.

Async Fetch Started
Asycn Cancel Result
Async Fetch Done

You’ll notice even though the cancellation is detected, it won’t stop the fetching job from completing.

It will perform till completion until Async Fetch Done is printed. However, it will not call the await subsequent code, as the await has stopped all the subsequent process of it since cancellation has been detected.

Image for post
Image for post

Although it won’t stop the fetch process, async-await is still better than launch approach as it can detect cancellation sooner.

Hopes from the above, you’ll get a taste of how to implement a complete basic network fetching work that handles Error, Exception, and Cancellation as well.

Mobile App Development Publication

Sharing Mobile App Development and Learning

Elye

Written by

Elye

Passionate about learning, and sharing mobile development and others https://twitter.com/elye_project https://www.facebook.com/elye.proj

Mobile App Development Publication

Sharing iOS, Android and relevant Mobile App Development Technology and Learning

Elye

Written by

Elye

Passionate about learning, and sharing mobile development and others https://twitter.com/elye_project https://www.facebook.com/elye.proj

Mobile App Development Publication

Sharing iOS, Android and relevant Mobile App Development Technology and Learning

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store