Retrofit 2.0 — A new way to work with Retrofit 2.0 in Android — Android Architecture

Ashiqul Islam Shaon
4 min readNov 17, 2020

As Developer, we love to write less code, because we are lazy :). Retrofit 2.0 is a popular library. Many developers use it normally, I mean the traditional way — create an ApiService Interface, a model, and a callback using RxJava or Call<T> interface or any other way.

Today we are going to learn a new way to work with Retrofit Library. Let’s dive into it.

Our model class

data class User(
val id: Int,
val name: String,
val phone: String
)

This is the universal ApiService class. This class will be used to get a response from our server.

interface ApiService {    @FormUrlEncoded
@POST("{url}")
suspend fun <T> postRequest(
@Path(value = "url", encoded = true) path: String,
@FieldMap hashMap: Map<String, String>
): Response<T>
@GET("{url}")
suspend fun <T> getRequest(
@Path(value = "url", encoded = true) path: String,
@QueryMap hashMap: Map<String, String>
): Response<T>
@Multipart
@POST("{url}")
suspend fun <T> sendDocuments(
@Path(value = "url", encoded = true) path: String,
@PartMap partMap: Map<String, @JvmSuppressWildcards RequestBody>,
@Part image: List<@JvmSuppressWildcards MultipartBody.Part>
): Response<T>
}

In your ApiHelper class

suspend fun createUser(mobileNo: String) = getResult(type = User::class.java) {
val hashMap = HashMap<String, String>()
hashMap["phone"] = mobileNo
getApiCall(CALL_TYPE_POST, "users", hashMap)
}

Here we created a function named createUser. This function will create a user. However, we call the getApiCall function inside the createUser function. There is also another function that returns the result.

getApiCall function determines which function we should hit from our ApiService interface. Hence we used a predefined variable called CALL_TYPE_POST. So, we are going to hit the post function from our ApiService interface.

In your ApiHelper class

//call type
private val CALL_TYPE_GET = "get"
private val CALL_TYPE_POST = "post"
private suspend fun <T> getApiCall(
type: String,
path: String,
hashMap: Map<String, Any>,
multiMartHashMap: List<MultipartBody.Part>? = null
): Response<T> {
return when (type) {
CALL_TYPE_GET -> {
apiService.getRequest(path, hashMap as Map<String, String>)
}
CALL_TYPE_POST -> {
apiService.postRequest(path, hashMap as Map<String, String>)
}
else -> {
apiService.sendDocuments(
path,
hashMap as Map<String, RequestBody>,
multiMartHashMap!!
)
}
}
}

All the code you need in your ApiHelper class

class ApiHelper(private val apiService: ApiService) : RetrofitBaseDataSource() {    //call type
private val CALL_TYPE_GET = "get"
private val CALL_TYPE_POST = "post"
private suspend fun <T> getApiCall(
type: String,
path: String,
hashMap: Map<String, Any>,
multiMartHashMap: List<MultipartBody.Part>? = null
): Response<T> {
return when (type) {
CALL_TYPE_GET -> {
apiService.getRequest(path, hashMap as Map<String, String>)
}
CALL_TYPE_POST -> {
apiService.postRequest(path, hashMap as Map<String, String>)
}
else -> {
apiService.sendDocuments(
path,
hashMap as Map<String, RequestBody>,
multiMartHashMap!!
)
}
}
}
suspend fun createUser(mobileNo: String) = getResult(type = User::class.java) {
val hashMap = HashMap<String, String>()
hashMap["phone"] = mobileNo
getApiCall(CALL_TYPE_POST, "users", hashMap) }
}

The main RetrofitBaseDataSource that we extend in our ApiHelper class. Let’s see what we have done in this class.

This is the main player. It ensures a safe and clean response to our UI.

getResult function is an inline function. It receives a class type that will help us later to determine which model class should be parsed and it receives a higher-order function.

After getting a response from the server, it handles the errors and passes the result to the UI using the Result sealed class.

abstract class RetrofitBaseDataSource {    protected suspend inline fun <T : Any> getResult(
type: Type,
crossinline call: suspend () -> Response<T>
): Flow<Result<T>> {
return flow {
emit(Result.InProgress(true))
val response = call() emit(
if (response.isSuccessful) {
val body = response.body()
if (body != null) {
Result.Success(body, type)
} else {
Result.UnknownError(null)
}
} else {
Result.ApiError(response.errorBody()!!, response.code())
}
)
emit(Result.InProgress(false))
}.catch { e ->
emit(Result.InProgress(false))
emit(
if (e is IOException) {
Result.NetworkError(e)
} else {
Result.UnknownError(e)
}
)
}.flowOn(Dispatchers.IO)
} }

Result class

sealed class Result<out R> {
data class Success<T>(val data: T, val type: Type) : Result<T>()

data class ApiError(val errorBody: ResponseBody, val code: Int) : Result<Nothing>()

data class NetworkError(val error: IOException) : Result<Nothing>()

data class UnknownError(val exception: Throwable?) : Result<Nothing>()

data class InProgress(val isLoading: Boolean) : Result<Nothing>()

}

Let’s hit the createUser function from our ViewModel to get a response.

In your ViewModel

fun createUser(mobileNo: String) {
if (mobileNo.isMobileValid()) {
executeSuspendedFunction { dataManager.apiHelper.createUser(mobileNo) }
} else
_snackBarMsg.value = context.getString(R.string.mobile_no_valid_msg)
}

Success response in your activity or fragment

So we get a response now we need to handle it on the UI side. so we created a function called handleSuccessfulResponse.

vm.result.observe(this, {
it
?.let {
when (it) {
is Result.InProgress -> {
if (it.isLoading)
showProgressing()
else
hideProgressing()
}
is Result.Success -> {
handleSuccessfulResponse(it)
}
}
}
}
)
private fun handleSuccessfulResponse(success: Result.Success<Any>) {
when (success.type) {
User::class.java -> {
val data = Gson().fromJson(success.data.toString(), User::class.java)

}
}
}

See the above function, We are using class type to determine which response we received at the UI side.

In your BaseViewModel

Here we are handling the response that we receive from the getResult function.

private val _result = MutableLiveData<Result<Any>>()
val result: LiveData<Result<Any>>
get() = _result
fun executeSuspendedFunction(codeBlock: suspend () -> Flow<Result<Any>>) {
viewModelScope.launch {
codeBlock().collect { result ->
_result.value = result
when (result) {
is Result.ApiError -> {
Log.d("RetrofitResult", result.errorBody.toString())
}
is Result.NetworkError -> {
Log.d("RetrofitResult", result.error.toString())
result.error.printStackTrace()
}
is Result.UnknownError -> {
result.exception?.printStackTrace()
Log.d("RetrofitResult", result.exception.toString())
}
}
}
}
}

Here we attach the response to our livedata variable and handling the errors.

See the whole architecture here

https://github.com/shaon2016/Android-CleanArchitecture-MVVM-HILT

--

--