跪拜 Guibai
← Back to the summary

How Retrofit Turns Callbacks into Kotlin Suspend Functions

Today's topic is a freebie, but many people still don't understand it, so let's talk about it today.

For example:

interface UserApi {

    @GET("user")
    suspend fun getUser(): User

}

Calling it:

val user = api.getUser()

Here, Retrofit is not synchronously waiting for the network result.

It simply converts the original asynchronous flow into a suspend-and-resume flow that Kotlin coroutines can understand. So you don't need to wrap it in withContext again.


How does Retrofit know this is a suspend method?

After compilation, a Kotlin suspend function adds a hidden parameter:

Object getUser(
    Continuation<? super User> continuation
)

In other words:

suspend fun getUser(): User

is actually similar to:

getUser(Continuation)

When Retrofit parses the interface method, it checks the method parameters.

If it finds that the last parameter is:

Continuation

it considers:

This is a Kotlin suspend method.

How does Retrofit handle suspend methods internally?

Core class:

HttpServiceMethod

Retrofit creates different handling methods based on different return types.

For:

suspend fun getUser(): User

it creates:

SuspendForBody

It is responsible for converting:

Call<User>

into:

User

What does SuspendForBody do internally?

The core logic is similar to:

suspend fun <T> Call<T>.await(): T {

    return suspendCancellableCoroutine { continuation ->

        enqueue(object : Callback<T> {

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

                continuation.resume(
                    response.body()!!
                )

            }


            override fun onFailure(
                call: Call<T>,
                error: Throwable
            ) {

                continuation.resumeWithException(error)

            }

        })
    }
}

It mainly accomplishes three things:

  1. Initiates an asynchronous request
enqueue()
  1. Saves the current coroutine execution state
Continuation
  1. Resumes the coroutine after the request completes

Success:

continuation.resume()

Failure:

continuation.resumeWithException()

Here, suspendCancellableCoroutine converts the original callback into a suspend function.


Is this await provided by Kotlin coroutines?

No.

The await() here:

await()

is just an extension function defined by Retrofit.

It's just a name.

What really matters is:

suspendCancellableCoroutine

and:

Continuation.resume()

What is the await in Kotlin coroutines then?

Kotlin coroutines also have an await:

val result = async {
    request()
}.await()

This belongs to:

Deferred.await()

Its role:

Wait for another coroutine task to complete.

Whereas Retrofit's:

Call.await()

Its role:

Convert the network request result into a coroutine result.

The two are not the same thing.


Does Retrofit's suspend block threads at the low level?

No.

It does not:

Thread waiting for network

Instead:

Call interface

↓

Coroutine suspends

↓

Thread released

↓

OkHttp executes network request

↓

Result returns

↓

Continuation.resume()

↓

Coroutine continues execution

What it waits for is:

An asynchronous result


Does Retrofit's suspend switch to Dispatchers.IO?

Retrofit is not responsible for thread scheduling.

Network thread:

OkHttp Dispatcher

Coroutine resumption:

Coroutine Dispatcher

The two have different responsibilities.

Why can exceptions be directly caught with try-catch then?

Because Retrofit converts Callback failures into coroutine exceptions.

Before:

onFailure()

The exception was inside the callback.

Now:

continuation.resumeWithException(error)

After the coroutine state machine receives the exception:

It is equivalent to:

throw error

Therefore:

try {
    api.getUser()
} catch(e:Exception){

}

can catch it.


Summary of Retrofit's suspend principle

Retrofit's suspend support is essentially about identifying suspend methods through dynamic proxies, using Continuation to convert OkHttp's asynchronous results into a coroutine suspend-and-resume flow, turning a Callback-style API into a suspend-style API.