Encapsulating Retrofit with Kotlin Coroutines in Android MVVM Architecture
This article explains how to integrate Retrofit with Kotlin coroutines following Google’s Android architecture guidelines, covering coroutine basics, OkHttp and Retrofit setup, generic response handling, repository abstraction, ViewModel usage, and lifecycle‑aware coroutine scopes to achieve clean, asynchronous network calls.
Retrofit 2.6.0+ provides native support for Kotlin coroutines, allowing asynchronous network requests to be written in a synchronous style. Combined with Jetpack components, this enables elegant network handling that aligns with Google’s recommended Android architecture.
The architecture separates concerns: UI (View) observes LiveData, ViewModel initiates requests, Repository encapsulates data fetching, and data models are defined as generic response templates.
Understanding Coroutines
A simple coroutine example demonstrates launching on the main thread, switching to Dispatchers.IO for a simulated network delay, and updating the UI on the main thread. Coroutines consist of CoroutineScope (e.g., GlobalScope ), CoroutineContext (e.g., Dispatchers.Main ), builders ( launch , async , runBlocking ), and suspend functions.
Coroutines work by transforming suspend functions into continuation‑passing style (CPS), eliminating manual callbacks and enabling sequential‑style asynchronous code.
Building OkHttp
object HttpClient {
private val okHttpClient: OkHttpClient by lazy {
OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply {
level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.BASIC
}
})
.build()
}
}The singleton pattern with lazy ensures a single OkHttpClient instance.
Building Retrofit
object HttpClient {
private val retrofits = HashMap
(8)
fun
getService(service: Class
, baseUrl: String): S {
return retrofits.getOrPut(baseUrl) {
Retrofit.Builder()
.client(okHttpClient)
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create())
.build()
}.create(service)
}
inline fun
getService(baseUrl: String): S =
getService(S::class.java, baseUrl)
}Each baseUrl gets its own Retrofit instance while sharing the same OkHttp client.
Declaring ApiService
interface ApiService {
@GET("home/feed")
suspend fun getHomeFeed(@Query("cityId") cityId: Int): HomeFeedDataAndResponse
}Using suspend on the function makes Retrofit return the deserialized object directly without a CallAdapter.
Unified Result Wrapper
sealed class HttpResult
{
data class Success
(val data: T) : HttpResult
()
data class Failure(val reason: String) : HttpResult
()
data class Error(val exception: Exception) : HttpResult
()
override fun toString(): String = when (this) {
is Success<*> -> "Success[data=$data]"
is Failure -> "Failure[reason=$reason]"
is Error -> "Error[exception=$exception]"
}
}The sealed class distinguishes success, business‑level failure, and network/error states.
Repository Implementation
object XXXRepository {
suspend fun
executeResponse(response: XXXResponse
): HttpResult
{
return if (response.code != 200) {
HttpResult.Failure(response.toString())
} else {
HttpResult.Success(response.data)
}
}
private val service = HttpClient.getService
("http://app.demo.com/")
suspend fun getHomeFeed(cityId: Int): HttpResult
{
return call { service.getHomeFeed(cityId) }
}
suspend fun
call(request: suspend () -> XXXResponse
): HttpResult
{
return try {
executeResponse(request())
} catch (e: Exception) {
HttpResult.Error(e)
}
}
}A high‑order call function centralises try‑catch handling, converting exceptions to HttpResult.Error .
Handling Multiple Response Templates
When different APIs return distinct JSON structures, separate response classes (e.g., XXXResponse , YYYResponse ) and repositories are created. Common logic is extracted into an abstract BaseRepository with an abstract executeResponse method that each concrete repository implements.
abstract class BaseRepository {
suspend fun
call(request: suspend () -> Any): HttpResult
{
return try { executeResponse(request()) } catch (e: Exception) { HttpResult.Error(e) }
}
abstract fun
executeResponse(response: Any): HttpResult
}Concrete repositories override executeResponse to handle their specific response type.
Using the Repository in ViewModel
val homeFeed = MutableLiveData
>()
fun getHomeFeed(cityId: Int) {
viewModelScope.launch {
homeFeed.value = XXXRepository.getHomeFeed(cityId)
}
}The UI observes homeFeed and reacts to Success , Failure , or Error states.
Tips
Prefer viewModelScope over GlobalScope because it is tied to the ViewModel’s lifecycle; when the ViewModel is cleared, the coroutine is automatically cancelled, preventing memory leaks.
Retrofit’s coroutine adapter internally uses enqueue() on a thread pool, so explicit withContext(Dispatchers.IO) is not required.
Conclusion
By following Google’s Android architecture guide, the app achieves a clear separation of concerns: ViewModel handles UI state via LiveData, Repository manages data fetching and error handling, and coroutines provide a concise, synchronous‑style API for asynchronous network calls without blocking the UI thread.
Sohu Tech Products
A knowledge-sharing platform for Sohu's technology products. As a leading Chinese internet brand with media, video, search, and gaming services and over 700 million users, Sohu continuously drives tech innovation and practice. We’ll share practical insights and tech news here.
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.