Mobile Development 14 min read

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.

Yang Money Pot Technology Team
Yang Money Pot Technology Team
Yang Money Pot Technology Team
Encapsulating Retrofit with Kotlin Coroutines in Android: Two Approaches

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/

DSLAndroidException HandlingnetworkKotlincoroutinesRetrofit
Yang Money Pot Technology Team
Written by

Yang Money Pot Technology Team

Enhancing service efficiency with technology.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.