Retrofit synchronous vs asynchronous communications

How can we perform the two types of operations in Retrofit

For a more complete and in-depth explanation, check out our complete Retrofit course

Download the starter project here

Github repository

Chances are you already know the differences between synchronous and asynchronous communications. But to put it simply synchronous will perform an operation and wait for the result while asynchronous means that an operation is started, you continue on with your processing and when the result comes in, then you process in any way you need.

The good news is that Retrofit by default can perform both types of operations.

Let’s have a look at an overview from an Android Retrofit point of view between sync and async communication.


That means sending a request and then continuing on with the execution. It is exactly what we’ve done so far in our tutorial. When the result of the operation comes in then we can proceed and do something with that result.

The key point here is that Retrofit handles this for us. It performs the operation on a background thread, and when the result comes in, it delivered the result in the main thread for us, so that we can use that result directly without having to switch threads.


That’s slightly different. We send our request, we block execution and then we resume only when the result comes in, whether the result is successful or not.

Importantly, in Android development, all network communication must be done on a background thread. If we perform a synchronous operation in Retrofit, it won’t handle threads for us. So that means we have to handle threads ourselves. If we try to perform an operation in a synchronous fashion, then the system will block, the application will crash and we will get a NetworkOperationOnMainThread error. You might have seen this kind of errors before.

That is the main issue with synchronous operations, we have to manage threads ourselves.

In this tutorial we will implement a synchronous operation using coroutines. We will see how we can use very simple coroutine functionality.

I won’t go into too much detail on using coroutines because there is a lot of information there.

If you’re interested in finding out more about that I have a course specifically related to that

Complete Coroutines in Kotlin


We have already implemented the asynchronous operation in previous tutorials.

call.enqueue(object: Callback<ApiCallResponse> {

    override fun onResponse(call: Call<ApiCallResponse>, response: Response<ApiCallResponse>) { }

    override fun onFailure(call: Call<ApiCallResponse>, t: Throwable) { }


The call that is returned to us by the api has an operation .enqueue that has two callbacks, onResponse and onFailure. These correspond to the success and failure of the network call and we can use them to add functionality to our reply.


The synchronous call looks simpler, but it actually isn’t because there are other tasks that we need to perform.


At this point the system will pause the thread until the outcome of this operation is available.

Like we said, we’re not allowed to perform this operation on the main thread.

To implement that, we need to update the MainViewModel and add a new function. We also need to add a job handler that will be disposed of in onCleared. We also need an exception handler for our coroutine.

var job: Job? = null

val exceptionHandler = CoroutineExceptionHandler { coroutineContext, throwable ->

    GlobalScope.launch(Dispatchers.Main) {

        onError(“Exception: ${throwable.localizedMessage}”)



fun fetchDataSync() {

    loading.value = true

    job = CoroutineScope(Dispatchers.IO + exceptionHandler).launch {

        val response =

        withContext(Dispatchers.Main) {

            if (response.isSuccessful) {

                val body = response.body()

                apiResponse.value = body?.flatten()

                error.value = null

                loading.value = false

            } else {

                onError(“Error: ${response.message()}”)





override fun onCleared() {




Make sure you also update the MainActivity to call this new function


If we’ve implemented everything correctly, running the code will give us the same output that we had previously, even though we’re now using synchronous communication (in a coroutine)

For a more complete and in-depth explanation, check out our complete Retrofit course

Close Bitnami banner