Encapsulating Retrofit with Kotlin Coroutines in Android: Two Approaches
This article explains how to combine Retrofit with Kotlin coroutines on Android, compares the traditional RxJava method with coroutine-based implementations, and presents two encapsulation techniques—using a DSL and leveraging CoroutineExceptionHandler—to simplify asynchronous network calls and centralize error handling.
Coroutine Introduction
1. Introduction
Developers familiar with Go, Python, and other languages may already know the concept of coroutines, which are not language‑specific but a programming paradigm for non‑preemptive task scheduling, allowing a program to voluntarily suspend and resume execution. Kotlin describes them as lightweight threads.
Coroutines differ from threads: both run on threads, but coroutines can be single‑ or multi‑threaded, whereas threads belong to a process, making their nature fundamentally different.
This article focuses on how to wrap Retrofit with coroutines; detailed coroutine theory is omitted, with references provided at the end.
2. Why Use Coroutines
Coroutines consume fewer resources than threads and execute more efficiently. In Android network development, combining coroutines with Retrofit simplifies asynchronous code and avoids "callback hell" when multiple API calls are needed.
Traditional Retrofit + RxJava approach:
private fun getUserInfo() {
apiService.getUserInfo()
.subscribe(object : Observer
> {
fun onNext(response: Response
) {
apiService.getConfiguration()
.subscribe(object : Observer
> {
fun onNext(response: Response
) {
// render data
}
})
}
})
}Retrofit + coroutine approach:
private fun getUserInfo() {
launch {
val user = withContext(Dispatchers.IO) { apiService.getUserInfo() }
val config = withContext(Dispatchers.IO) { apiService.getConfiguration() }
// render data
}
}The coroutine version replaces nested callbacks with sequential code, improving readability and maintainability.
3. Why Encapsulate
Both examples lack exception handling. Real‑world network requests encounter various errors (e.g., UnknownHostException, CertificateException, JsonParseException) and business errors such as token expiration. A complete solution must handle these uniformly.
class MainActivity : BaseActivity(), CoroutineScope by MainScope() {
fun getUserInfo() {
launch {
try {
val data = withContext(Dispatchers.IO) { apiService.getUserInfo() }
if (data.isSuccess) {
// render data
} else {
// handle server error
}
} catch (e: Exception) {
// handle request exception
}
}
}
}Repeating this boilerplate for every request is cumbersome, so encapsulation is needed, primarily to centralize error handling.
Solution 1 – Using a DSL
1. Usage
After encapsulation, a request can be written in three lines:
fun getUserInfo() {
requestApi
{
response = apiService.getUserInfo()
onSuccess { /* handle success */ }
onError { /* handle error, optional */ }
}
}2. Implementation
We define a DSL class and an extension function on BaseActivity :
class HttpRequestDsl
: IResponse {
lateinit var response: T
internal var onSuccess: ((T) -> Unit)? = null
internal var onError: ((Exception) -> Unit)? = null
fun onSuccess(block: ((T) -> Unit)?) { this.onSuccess = block }
fun onError(block: ((Exception) -> Unit)?) { this.onError = block }
}
fun
BaseActivity.requestApi(dsl: suspend HttpRequestDsl
.() -> Unit) {
launch {
val httpRequestDsl = HttpRequestDsl
()
try {
withContext(Dispatchers.IO) { httpRequestDsl.dsl() }
val response = httpRequestDsl.response
if (response.isSuccess) {
httpRequestDsl.onSuccess?.invoke(response)
} else {
httpRequestDsl.onError?.invoke(ApiErrorException(response))
}
} catch (e: Exception) {
if (httpRequestDsl.onError == null) {
handleCommonException(e)
} else {
httpRequestDsl.onError!!.invoke(e)
}
}
}
}3. Summary
The DSL reduces a verbose network call to a few concise lines, but the callback‑style onSuccess/onError remains, which may feel redundant when using coroutines.
Solution 2 – Leveraging CoroutineExceptionHandler
1. Usage
With a global CoroutineExceptionHandler , the request code contains no explicit try‑catch:
class MainActivity : BaseActivity() {
fun getUserInfo() {
mainScope.launch {
val data = withContext(Dispatchers.IO) { apiService.getUserInfo() }
// render data
}
}
}2. Implementation
We create a handler that distinguishes different exception types:
class CoroutineExceptionHandlerImpl : CoroutineExceptionHandler {
override val key = CoroutineExceptionHandler
override fun handleException(context: CoroutineContext, exception: Throwable) {
when (exception) {
is ApiErrorException -> { /* handle business error */ }
is JsonParseException -> { /* handle parsing error */ }
is CertificateException, is SSLHandshakeException -> { /* handle cert errors */ }
else -> { /* other errors */ }
}
}
}Base activity defines a mainScope with this handler:
abstract class BaseActivity : AppCompatActivity() {
protected val mainScope = MainScope() + CoroutineExceptionHandlerImpl()
}Business errors can be thrown as ApiErrorException from a custom Retrofit ConverterFactory :
class ResponseConverter
: Converter
{
override fun convert(value: ResponseBody): T? {
val result = adapter.fromJson(value.string())
if (result is IResponse && !result.isSuccess) {
throw ApiErrorException(result)
}
return result
}
}Specific API error handling can still use a try‑catch around the call when needed:
class MainActivity : BaseActivity() {
fun getUserInfo() {
mainScope.launch {
try {
val data = withContext(Dispatchers.IO) { apiService.getUserInfo() }
// render data
} catch (e: ApiErrorException) {
(e.response as? UserResponse)?.let {
// custom handling for login failure, etc.
}
}
}
}
}3. Summary
Solution 2 moves most error handling to a centralized handler, reducing repetitive try‑catch blocks while preserving coroutine’s sequential style; however, fine‑grained business‑specific handling may still require localized catches.
Conclusion
The two encapsulation methods each have strengths and trade‑offs. The DSL approach offers concise syntax but retains callback‑style hooks, whereas the CoroutineExceptionHandler approach yields cleaner sequential code with centralized error processing, suitable for simpler projects or when combined with Jetpack components.
Coroutines provide far more capabilities beyond the examples shown; continued exploration will reveal deeper Kotlin advantages.
References:
Android Kotlin Coroutines – https://developer.android.com/kotlin/coroutines?hl=zh-cn
Kotlin Coroutines Documentation – https://www.kotlincn.net/docs/reference/coroutines/coroutines-guide.html
Breaking Down Kotlin Coroutines – https://www.bennyhuo.com/2019/04/01/basic-coroutines/
Why Coroutines Are Called “Lightweight Threads” – https://www.bennyhuo.com/2019/10/19/coroutine-why-so-called-lightweight-thread/
Yang Money Pot Technology Team
Enhancing service efficiency with technology.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.