Android networking and database caching in 2020 (MVVM+Retrofit+Room+Flow)

Android networking and database caching in 2020 (MVVM+Retrofit+Room+Flow)

MVVM is the new love of Android developers. It provides lots of benefits like clean architecture, code maintainability etc. However, great power comes with great responsibilities. You have to maintain networking, a database and a lot more. and if you want to handle all these things in a single place, then you’ll end up with ugly-looking code. Fortunately, we have a solution for it now. In this article, I’m going to cover what problems I faced and how my library is going to solve them, as it will help you to write concise and clean code with more control over your code.

The first problem I encountered was, how to handle the database and networking in one place. where to handle retrofit error response if network call fails? Where to get data from, if the network call fails? etc.

The second problem was, I wanted to work with the latest tech, so I chose to go with Kotlin flow instead of live data. So how to organize all these things with flow? Is there any algorithm that handles all these things? I decided to write a new library for it. So, here’s how I implemented it.

Before we start exploring, these are 3 requirements that must be satisfied to implement this library:

  • Dao method must return Flow<ModelClass>

  • Api method must return Flow<ApiResponse<ModelClass>>

  • We need to add FlowCallAdapterFactory as CallAdapterFactory in the retrofit builder to let retrofit know how to convert a response into our Flow<ApiResponse<>>wrapper. (it’s implemented as part of the library)

We’ll consider that our network call will have either of the three states: SUCCESS, ERROR or LOADING at any time. So, we need to create a class as below.

data class Resource<out T>(
    val status: Status,
    val data: T?,
    val message: String?
) {

    enum class Status {
        SUCCESS,
        ERROR,
        LOADING
    }

    companion object {
        fun <T> success(data: T?): Resource<T> {
            return Resource(
                Status.SUCCESS,
                data,
                null
            )
        }

        fun <T> error(msg: String, data: T? = null): Resource<T> {
            return Resource(
                Status.ERROR,
                data,
                msg
            )
        }

        fun <T> loading(data: T? = null): Resource<T> {
            return Resource(
                Status.LOADING,
                data,
                null
            )
        }
    }
}

Now, we’ll consider that our API client (In this case Retrofit), will return either of three responses:

  • Success Response — When the network call is successful

  • Error Response — When network call is failed (including exceptions thrown by retrofit)

  • Empty Response— When the success response has no body (HTTP 204), we’ll use this class

So, our implementation will look like this

data class Resource<out T>(
    val status: Status,
    val data: T?,
    val message: String?
) {

    enum class Status {
        SUCCESS,
        ERROR,
        LOADING
    }

    companion object {
        fun <T> success(data: T?): Resource<T> {
            return Resource(
                Status.SUCCESS,
                data,
                null
            )
        }

        fun <T> error(msg: String, data: T? = null): Resource<T> {
            return Resource(
                Status.ERROR,
                data,
                msg
            )
        }

        fun <T> loading(data: T? = null): Resource<T> {
            return Resource(
                Status.LOADING,
                data,
                null
            )
        }
    }
}

Now comes the real gem, NetworkBoundResource file

inline fun <DB, REMOTE> networkBoundResource(
    crossinline fetchFromLocal: () -> Flow<DB>,
    crossinline shouldFetchFromRemote: (DB?) -> Boolean = { true },
    crossinline fetchFromRemote: () -> Flow<ApiResponse<REMOTE>>,
    crossinline processRemoteResponse: (response: ApiSuccessResponse<REMOTE>) -> Unit = { Unit },
    crossinline saveRemoteData: (REMOTE) -> Unit = { Unit },
    crossinline onFetchFailed: (errorBody: String?, statusCode: Int) -> Unit = { _: String?, _: Int -> Unit }
) = flow<Resource<DB>> {

    emit(Resource.loading(null))

    val localData = fetchFromLocal().first()

    if (shouldFetchFromRemote(localData)) {

        emit(Resource.loading(localData))

        fetchFromRemote().collect { apiResponse ->
            when (apiResponse) {
                is ApiSuccessResponse -> {
                    processRemoteResponse(apiResponse)
                    apiResponse.body?.let { saveRemoteData(it) }
                    emitAll(fetchFromLocal().map { dbData ->
                        Resource.success(dbData)
                    })
                }

                is ApiErrorResponse -> {
                    onFetchFailed(apiResponse.errorMessage, apiResponse.statusCode)
                    emitAll(fetchFromLocal().map {
                        Resource.error(
                            apiResponse.errorMessage,
                            it
                        )
                    })
                }
            }
        }
    } else {
        emitAll(fetchFromLocal().map { Resource.success(it) })
    }
}

In NetworkBoundResource file, we’re passing some function inside a function (Kotlin’s higher-order function) that will decide what should be done in certain cases. Here’s a quick explanation of each class that we’re passing inside networkBoundResource function:

  • fetchFromLocal — It fetch data from the local database

  • shouldFetchFromRemote — It decides whether network request should be made or use local persistent data if available (Optional)

  • fetchFromRemote — It performs network request operation

  • processRemoteResponse — It process the result of the network response before saving the model class in the database, like saving certain header values (Optional)

  • saveRemoteData — It saves the result of a network request to local persistent database

  • onFetchFailed — It handles network request failure scenarios (Non HTTP 200..300 response, exceptions etc.) (Optional)

Now when using it, this is how repository class will look like

fun getSomething(): Flow<Resource<ModelClass>> {
    return networkBoundResource(
      fetchFromLocal = { daoClass.getFromDatabase() },
      shouldFetchFromRemote = { it == null },
      fetchFromRemote = { apiInterface.getFromRemote() },
      processRemoteResponse = { },
      saveRemoteData = { daoClass.saveRemoteData(it) },
      onFetchFailed = { _, _ -> }
      ).flowOn(Dispatchers.IO)
}

And our view model class will look like this,

val data: LiveData<Resource<ModelClass>> = repository.getSomething().map {
  when (it.status) {
    Resource.Status.LOADING -> {
        Resource.loading(null)
    }
    Resource.Status.SUCCESS -> {
        Resource.success(it.data)
    }
    Resource.Status.ERROR -> {
        Resource.error(it.message!!, null)
    }
 }
}.asLiveData(viewModelScope.coroutineContext)

Now we can observe this variable in our UI and drive UI based on it.

You don’t need to do all this fancy stuff, as it’s available as a library on GitHub. You just need to implement your project-specific code in your repository and ViewModel class.

If it helps you to write clean and concise code, press the clap button 😃

You can find a sample app built on this library on GitHub.