Skip to content

3 Ways to use Retrofit with LiveData in the MVVM Android Architecture

Back in the good ol’ days of MVP, the combination of RxJava and Retrofit was almost an absolute. It only made perfect sense to return API calls as Observables that can be easily mapped and manipulated in ways limited only by imagination.

While MVVM can still just as easily make use of RxJava, there are reasons why you wouldn’t want to (that we won’t get into here) and instead, choose the wonderful world of LiveData.

Why doesn’t LiveData work so well with API calls, you may ask? We could potentially create an adapter that maps the Retrofit Call to the LiveData, but the underlying problem here is the lack of the ability to return an error value. There is a workaround to this which we have as one of our 3 solutions, but we’ll get to that later.

For this tutorial, I’ll be using the JSONPlaceholder API. Particularly, this one:

https://jsonplaceholder.typicode.com/posts

This tutorial does of course, require you to know about MVVM, LiveData, and Retrofit. We’ll also be going through as if we already have Retrofit set up in our app.

Method 1: Using Standard Calls

We’ll start off, firstly with the normal way to do it. Use your repository to enqueue a call and update your LiveData in its callbacks.

fun getPosts() {
    jsonPlaceHolderApi.getPosts()
        .enqueue(object: Callback<List<Post>> {
            override fun onResponse(call: Call<List<Post>>?, response: Response<List<Post>>) {
                if (response.isSuccessful)
                    posts.postValue(response.body())
            }
            override fun onFailure(call: Call<List<Post>>?, error: Throwable?) {
                handleError(error)
            }
        })
}

While easily quite stable and easy to use, piling callbacks like this can get messy very quickly… and that’s pretty much the main disadvantage of this method.

Method 2: Using Coroutines

As of Retrofit 2.6.0, we now have official support for coroutines.

We’ll need to add a few extra dependencies

implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-rc02'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.2.0-rc02'
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0-rc02'

Also, make sure your Kotlin is up to date with the latest version to ensure coroutines are working.

With that, we simply change our API call from this

@GET("posts")
fun getPosts(): Call<List<Post>>

To this

@GET("posts")
suspend fun getPosts(): List<Post>

Then in our repository, we change our getPosts() function to this sexy one-liner

suspend fun getPosts() = jsonPlaceHolderApi.getPosts()

Then our ViewModel function changes to this

fun getPosts() = liveData(Dispatchers.IO) {
    emit(repository.getPosts())
}

No change is required in our Activity, as it should be.

While easily the most concise solution here, it’s not without its problems. Firstly, since we are making use of dependencies that are still in their alpha builds, we can’t guarantee stability. Suspend functions also don’t yet support null API response bodies, but that’s an issue in the works.

Method 3: Creating our own Adapter

This solution was created by pavincii, I suggest you go check him out.

This method takes advantage of the fact we can set our own Call Adapters to our Retrofit instance. Here’s the adapter panincii made

class LiveDataCallAdapter<R>(private val responseType: Type): CallAdapter<R, LiveData<ApiResponse<R>>> {
    override fun adapt(call: Call<R>): LiveData<ApiResponse<R>> {
        return object : LiveData<ApiResponse<R>>() {
            private var isSuccess = false

            override fun onActive() {
                super.onActive()
                if (!isSuccess) enqueue()
            }

            override fun onInactive() {
                super.onInactive()
                dequeue()
            }

            private fun dequeue() {
                if (call.isExecuted) call.cancel()
            }

            private fun enqueue() {
                call.enqueue(object : Callback<R> {
                    override fun onFailure(call: Call<R>, t: Throwable) {
                        postValue(ApiResponse.create(UNKNOWN_CODE, t))
                    }

                    override fun onResponse(call: Call<R>, response: Response<R>) {
                        postValue(ApiResponse.create(response))
                        isSuccess = true
                    }
                })
            }
        }
    }

    override fun responseType(): Type = responseType
}

We’ll also need to define the class ApiResponse

import retrofit2.Response

internal const val UNKNOWN_CODE = -1

sealed class ApiResponse<T> {
    companion object {
        fun <T> create(response: Response<T>): ApiResponse<T> {
            return if (response.isSuccessful) {
                val body = response.body()
                if (body == null || response.code() == 204) {
                    ApiEmptyResponse()
                } else {
                    ApiSuccessResponse(body)
                }
            } else {
                ApiErrorResponse(response.code(), response.errorBody()?.string()?:response.message())
            }
        }

        fun <T> create(errorCode: Int, error: Throwable): ApiErrorResponse<T> {
            return ApiErrorResponse(errorCode, error.message ?: "Unknown Error!")
        }
    }
}

class ApiEmptyResponse<T> : ApiResponse<T>()
data class ApiErrorResponse<T>(val errorCode: Int, val errorMessage: String): ApiResponse<T>()
data class ApiSuccessResponse<T>(val body: T): ApiResponse<T>()

The reason for this is that LiveData doesn’t provide any error callbacks so we need to have a way to handle them.

Then we need to create a factory for this adapter

class LiveDataCallAdapterFactory: CallAdapter.Factory() {
    override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
        val observableType =
            CallAdapter.Factory.getParameterUpperBound(0, returnType as ParameterizedType) as? ParameterizedType
                ?: throw IllegalArgumentException("resource must be parameterized")
        return LiveDataCallAdapter<Any>(CallAdapter.Factory.getParameterUpperBound(0, observableType))
    }
}

We then update our Retrofit Builder to include this Call Adapter

val retrofit = Retrofit.Builder()
    .baseUrl("https://jsonplaceholder.typicode.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(LiveDataCallAdapterFactory())
    .build(

Then finally, we make our Api call return a LiveData

@GET("posts")
fun getPosts(): LiveData<ApiResponse<List<Post>>>

It does have a few inherent problems like how are we going to refresh data? This feels more like a use-case for the equivalent of RxJava’s Single, rather than an Observable.

Tags: