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
When using coroutines usually the solution to deal with errors is wrapping your calls with try/catch. That's what we are about to do, but we don't want that spread all over our code, so it's important to keep it in a single place in order to make it less error prone and avoid repeated code.
When using CoroutineExceptionHandler, if one coroutine fails, then all child coroutines will get cancelled by default. In order for a CoroutineExceptionHandler to have an effect, it must be installed either in the CoroutineScope or in a top-level coroutine. Now moving ahead to handle exceptions with async/await.
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?
Certainly, you are correct. The coroutine should fire on the ViewModel
even though the logic is in the Repository
/Service
. That's why Google has already created a special coroutineScope
called viewModelScope, otherwise it would be "repositoryScope" . Also coroutines have a nice feature on exception handling called the CoroutineExceptionHandler
. This is where things get nicer because you don't have to implement a try{}catch{}
block:
val coroutineExceptionHanlder = CoroutineExceptionHandler{_, throwable -> throwable.printStackTrace() toastLiveData.value = showToastValueWhatever() }
Later in the ViewModel
coroutineScope.launch(Dispatchers.IO + coroutineExceptionHanlder){ val data = serviceOrRepo.getData() }
Of course you can still use the try/catch
block without the CoroutineExceptionHandler
, free to choose.
Notice that in case of Retrofit you don't need the Dispatchers.IO
scheduler because Retrofit does that for you (since Retrofit 2.6.0).
Anyways, I can't say the article is bad, but it is not the best solution. If you want to follow the articles' guide, you may want to check Transformations on the LiveData.
EDIT: What you need to know more, is that coroutines are not safe. What I mean with this is that they might cause memory leaks especially in Android lifecycle overall. You need a way to cancel coroutines while the Activity
/Fragment
doesn't live anymore. Since the ViewModel
has the onCleared
(which is called when Activity
/Fragment
is destroyed), this implies that coroutines should fire in one of them. And perhaps this is the main reason why you should start the coroutine in the ViewModel
. Notice that with the viewModelScope
there is no need to cancel
a job onCleared
.
A simple example:
viewModelScope.launch(Dispatchers.IO) { val data = getDataSlowly() withContext(Dispatchers.MAIN) { showData(); } }
or without the viewModelScope
:
val job = Job() val coroutineScope = CoroutineContext(Dispatchers.MAIN + job) fun fetchData() { coroutineScope.launch() { val data = getDataSlowly() withContext(Dispatchers.MAIN) { showData(); } } } //later in the viewmodel: override fun onCleared(){ super.onCleared() job.cancel() //to prevent leaks }
Otherwise, your Service
/Repository
would leak. Another note is NOT to use the GlobalScope
in this cases.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With