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:
- Initiates an asynchronous request
enqueue()
- Saves the current coroutine execution state
Continuation
- 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.