This is one of series of Simple Android Architecture. and you can check sample code there.

In the Api call, There are 5 approaches explained on Kotlin coroutine Tutorial
This article will explain the difference of each. and why we have to use Coroutine. and how to use it simple.

  1. Threading
  2. Callback (ex: Listener)
  3. Chaining (ex: LiveData)
  4. Reactive Extensions (ex: RxJava)
  5. Coroutine

Index

  1. Example of 5 approaches
  2. Why use Coroutine
  3. Suggestion of Coroutine Usage

Prerequisite

In this article, I use these knowledge. If you don’t have experience of that, Kindly check the link below.

  • Resource : Resource is data which containing status (loading, success, error)
  • LiveResource : this is _LiveData_
  • LiveData : MediatorLiveData, Transformations.switchMap()
  • Retrofit2 : especially knowledge of CallAdapter
  • Kotlin Coroutine : this article handle how to use coroutine, instead what is Coroutine
  • MVVM, Observer pattern

Example of 5 approaches

I’ll show the example of calling 2 apis in sequence
You can see the difference by them.

Threading

You may be curious how to handle error. It’s explained on “Suggestion of Coroutine Usage” part below

load() function can be used for common. so, for two apis, we added just 2 line code only.
In code simplicity, readability, maintenance perspective, Thread looks nice.

But, There are performance drawbacks

So, in MVP period, we used callback even if the code readability not that good.

Callback

If you see postItem(), you can easily read because it shows business logic only

But, There are some drawbacks

  1. getToken()submitPost() is required to keep good readability. and they are boilerplate code which is not necessary in Threading approach.
  2. if the logic gets more complicated and need many asynchronous calls, we need much of lambda block and it increases code depth making code as tree
  3. comparing to top-down imperative approach(like Threading), It’s difficult to make logic like loop, conditional api call, etc.

see more drawbacks

That’s why Chaining or Reactive Extensions approaches come out.

Chaining

Actually, I call it ‘chaining’ because this approach chains the calls. but I don’t know proper designation.

plusAssign() : You can set the result of api call to result field by operator of ‘+=”
successSwitchMap() : switch LiveData only when it’s success. if it’s error, directly set error to result field. this can be used in common use
LiveResource : api returns LiveResource, it contains success and fail both case by CallAdapter

This approach has some merit below

  1. The code is simpler than Callback approach(just 2 line of code). and returns LiveResource which is already used in View and ViewModel side. so, you don’t need to study new concepts
  2. apis are called by chaining methods, not increasing the code depth unlike Callback approach.

But, There are some drawbacks as well.

  1. Though lambda block depth is not increased,
    Yet, each call is divided by switchMap(). so, it’s difficult to communicate between each swithMap code block
  2. There are detailed drawbacks

Reactive Extensions

Now, we are looking into RxJava.
Actually, I’m not sure I’m qualified to explain RxJava, as I didn’t have deep experience to use it. If There is something wrong, kindly understand me and let me know to fix it.

There are some merits of RxJava comparing to Chaining

  • A lot of developers use and contribute. so, it will evolve continuously
  • Easy error handling
  • Easy thread setting
  • Easy parallel api call
  • Lots of functional operators
  • Supporting lots of platforms and language. we can experience same in various platform and language.

And there is famous phrase for RxJava

everything is a stream, and it’s observable

Stream may be used for various cases to handle gracefully.

But, I don’t want to handle the cases on this article.

I’ll handle only two cases below. because, we mostly experience the two cases only when call api and handle UI

  • single data -> use suspend function
  • changeable data -> use LiveData

Why use Coroutine?

Finally, I introduce Coroutine.

When you see, the returned data is just data not wrapped by LiveResource or Flowable or Retrofit’s Response
It means that we don’t need new concept any more. we can return to Threading approach satisfying performance(Coroutine solves it by observer pattern which Callback, Chaining, RxJava approach also used.).

RxJava provides a lot of functional operators for supporting everything.
But, in other words, Chaining approach has a lot of limitation like looping, exception handling, conditional api call, parallel api call, communication between each calls, etc.
and RxJava is originated from Chaining, That’s why RxJava provides much of operators to cover them.

but, if we call the asynchronous api in synchronous way and the api returns actual data(not wrapped), we can process easily and straight-forwardly the complicated logic which the Chaining approach has to process complicatedly.

Suggestion of Coroutine Usage

The concept is same with the sample code of Coroutine.

As sample shows just simple case, I would like to explain how to handle complicated case below in simple way.

  • Handle sever’s logical error
  • Error handling in UI
  • Handle Error in Logic
  • Multiple api call in parallel
  • Api polling
  • Debounce
  • Call in idle(prevent duplicated api call by clicking button in a second)
  • Retry
  • Handle changes in Room database

Handle sever logical error

There are two suggestions

  • Handle error by try catch
  • Api returns only success data

Handle error by try catch

In the Coroutine example, you may find that errors are handled by ‘try catch’.

Normally, when we use synchronous api call, we handle errors by Resource or Response in Retrofit. because api can be failed, so returned value should contains status as well.

I used Retrofit’s Response for the example.

val tokenResponse = api.getToken()
if (tokenResponse.isSuccessful) {
    val submitResponse = api.submitPost(PostRequestBody(tokenResponse.body()!!, item))
    if (submitResponse.isSuccessful) {
        result.postSuccess(Unit)
    } else {
        result.postError()
    }
} else {
    result.postError()
}

You can see a lot of if else condition. and it makes difficult to understand the business logic. so that, difficult to maintain the code as well.

Then what we have to do?

Let’s think about other error cases like reading data from database, or converting String to Int. mostly we use exception and try catch.

If try catch is the common way of error handling,
instead of using different way, to unify error handling to try catch will make code simpler and maintainable.

So, I decided to throw exception if it’s not success case.
So that, we can focus on success case only.

Api returns only success data

Normally, we define response body like below

{  
    status : "success",  
    data : {  
        "posts" : [  
            { "id" : 1, "title" : "A blog post", "body" : "Some useful content" },  
            { "id" : 2, "title" : "Another blog post", "body" : "More content" },  
        ]  
     }  
}

The response body contains data and status.
It’s not matter whether server send data in this structure or just send success data. because, Retrofit CallAdapter can throw exception on fail case whatever it is.

But, I would like to suggest sever send only success data.

Then, How to handle error?
In response, there is a ‘code’ field which shows success or other system errors from 2xx ~ 5xx

As, the code use until 5xx only. what if we decide custom error code above 600 for logical error or 500 internal server error?
And, if it’s success, returns only success data. if it’s fail, returns only fail data

Then what is the benefit of it?
server side code also can be simple

When wrapped by ResponseBody (example used Spring framework)

@GetMapping
public ResponseBody<SomeData> getSomeData() {
   if (isFail) {//1st way of handling fail
      throw new CustomeException("unknown error")
   }
   if (isFail2) {//2nd way of handling fail
      return ResponseBody.fail(...)
   }
   return ResponseBody.success(new SomeData())
}
//for 1st way of handling fail
@ExceptionHandler(CustomException.class)
public ResponseEntity handleCustomException(HttpServletRequest request, CustomException exception) {
   new ResponseEntity<>(ResponseBody.fail(...), ..., HttpStatus.OK)
}

Without ResponseBody

@GetMapping
public SomeData getSomeData() {
   if (isFail) {//1st way of handling fail
      throw new CustomeException("unknown error")
   }
   return new SomeData()
}
@ExceptionHandler(CustomException.class)
public ResponseEntity handleCustomException(HttpServletRequest request, CustomException exception) {
   new ResponseEntity<>(FailData(...), ..., LOGICAL_ERROR_CODE)
}

You can see that returned value is just success data.

for me, it looks natural. in programming language, function return just data and if it’s fail, it just throw exception.

Error handling in UI

We covered, on the above, that how to process api call or asynchronous call.
Api returns actual data and throw exception on fail case.

we normally use LiveData for view side to show the data(maybe it’ll be changed if Jetpack Compose is released), Coroutine sample shows that LiveResource<T>.load() converts data and exception to Resource. so, we don’t need to set data to LiveData manually.

After that, view side handle Resource, and if it’s progressing, show progress bar. if it’s fail, we shows snack bar or toast or some message in layout, etc. like below.

viewModel.state.observe { it: Resource
    if (it.isLoading()) {
        progressDialog.show()
    } else {
        progressDialog.dismiss()
    }

    dismissSnackbar(binding.root)// if state is changed, dismiss snackbar if it's shown.

    it.onError {
        showErrorSnackbar(binding.root, it.retry)
    }
}

Normally, there are two cases to show error ui.

  • When Activity/Fragment is created
  • When user action like clicking button

For development efficiency and design consistency, we normally shows same format of error on UI.

in this case, Let’s base code do it automatically.

for detailed explanation, BaseViewModel has state and initState fields for common error handling and BaseActivity or BaseFragment observe them automatically.
so, if it’s common error ui case, you don’t need to add code for handle error ui.
just load data like below. that’s it (I explained about BaseViewModeBaseActivityBaseFragment on another article)

state.load {  
    api.getToken()  
}

Handle Error in Logic

in the above, I showed how view side handle error. but what if we have to handle error in viewModel side or use case side?

It’s very simple. handle it like normal fail case. just do try catch.

result.load {
    val token = try {
        api.getToken()
    } catch (e: ResourceException) {
        "guest token"
    }
    api.submitPost(PostRequestBody(token, item))
}

Multiple api call in parallel

It’s very simple as well.
What you have to do is to use async() Coroutine function and await()

Sometimes we call several apis in parallel, and we worry if there are 5 apis and 4 apis are success but 1 api is failed. do we have to call 5 apis again? or just call 1 api? it depends on business logic. but if you have to call the failed one api. It also simple to implement.

Api polling

After some transaction, client wait server’s decision if the transaction is success or fail. and client call status checking api repeatedly.

for that, we don’t need any special function to do that.

It’s very simple. and straight-forward to understand. isn’t it?

But, delay() or for() looks boilerplate code which is able to shorten to 1 function

It’s up to you whether to use utility function polling() or not.
whatever it is, it’s readable and simple.

Debounce

I found there is debounce operator in RxJava
So, I implement it on Coroutine way.

It’s quite simple, just add LiveResource.loadDebounce()

override fun <T> LiveResource<T>.loadDebounce(
    timeInMillis: Long,
    work: suspend CoroutineScope.() -> T
) {
    value?.onLoading { it?.cancel() }
    load {
        delay(timeInMillis)
        work()
    }
}

You can do by yourself, as the implementation is simple.
But, cancelling job, and delaying can be shorten to one function. so, I added loadDebounce function

Call in idle

state.loadInIdle {
    api.submitPost(...)
}
override fun <T> LiveResource<T>.loadInIdle(work: suspend CoroutineScope.() -> T) {
    if (value.isLoadingState()) {
        return
    }
    load(work)
}

I think no need more explanation. if it’s already called and loading, then ignore api call.

Retry

When api call is failed, it’s simple to retry, just call the api again.
But, if there is common error ui. and ui shows retry button.
we should set the click listener. and the listener call the api on every retry button.

So, I added retry field on Resource.Error. so that, view side can retry if Resource is Error

sealed class Resource<out T> {
    data class Loading(val job: Job? = null) : Resource<Nothing>()
    data class Success<T>(val data: T) : Resource<T>()
    data class Error(val error: ResourceError, val retry: () -> Unit = {}) : Resource<Nothing>() {
        init {
            log(error)
        }
    }
}

What you have to do is just call the api. then BaseViewModelBaseFragment will show snackbar, and when user click retry button, it will retry automatically.

Handle changes in Room database

I explained merit of Coroutine and suspend function.
But, There are some cases in which LiveData is required.
suspend function is only for single time.
but like the below. in some case, we need to observer changes. and in this case, we have to use LiveData instead of suspend function.

@Query("SELECT * FROM Tasks order by entryid")
fun getTasks(): LiveData<List<Task>>

You can check the example on the repository. you can test by running ‘sample’ module.

Any issue or feedback, Kindly let me know.
I’ll really appreciate it.

Happy coding :)