After reading this issue How to deal with exception and this Medium Android Networking in 2019 — Retrofit with Kotlin’s Coroutines I've created my solution which consist in a BaseService
capable of making the retrofit call and forward the results and exceptions down the "chain":
API
@GET("...")
suspend fun fetchMyObject(): Response<List<MyObject>>
BaseService
protected suspend fun <T : Any> apiCall(call: suspend () -> Response<T>): Result<T> {
val response: Response<T>
try {
response = call.invoke()
} catch (t: Throwable) {
return Result.Error(mapNetworkThrowable(t))
}
if (!response.isSuccessful) {
return Result.Error...
}
return Result.Success(response.body()!!)
}
ChildService
suspend fun fetchMyObject(): Result<List<MyObject>> {
return apiCall(call = { api.fetchMyObject() })
}
Repo
suspend fun myObjectList(): List<MyObject> {
return withContext(Dispatchers.IO) {
when (val result = service.fetchMyObject()) {
is Result.Success -> result.data
is Result.Error -> throw result.exception
}
}
}
Note: sometimes we need more than throwing an exception or one type of success result. To handle those situations this is how we can achieve that:
sealed class SomeApiResult<out T : Any> {
object Success : SomeApiResult<Unit>()
object NoAccount : SomeApiResult<Unit>()
sealed class Error(val exception: Exception) : SomeApiResult<Nothing>() {
class Generic(exception: Exception) : Error(exception)
class Error1(exception: Exception) : Error(exception)
class Error2(exception: Exception) : Error(exception)
class Error3(exception: Exception) : Error(exception)
}
}
And then in our ViewModel:
when (result: SomeApiResult) {
is SomeApiResult.Success -> {...}
is SomeApiResult.NoAccount -> {...}
is SomeApiResult.Error.Error1 -> {...}
is SomeApiResult.Error -> {/*all other*/...}
}
More about this approach here.
BaseViewModel
protected suspend fun <T : Any> safeCall(call: suspend () -> T): T? {
try {
return call()
} catch (e: Throwable) {
parseError(e)
}
return null
}
ChildViewModel
fun fetchMyObjectList() {
viewModelScope.launch {
safeCall(call = {
repo.myObjectList()
//update ui, etc..
})
}
}
I think the ViewModel
(or a BaseViewModel
) should be the layer handling the exceptions, because in this layer lies the UI decision logic, for example, if we just want to show a toast, ignore a type of exception, call another function etc...
What do you think?
EDIT: I've created a medium with this topic