kotlin coroutines version 1.3.8
kotlin 1.3.72
This is the first time I am using coroutines and I have converted my rxjava2 with using coroutines. But as this is my first time I am wondering if I am following best practices.
One question I have is catching the exceptions, as in kotlin this could be bad practice as swallowing exceptions might hide an servious bug. But using coroutines is there any other way to capture errors. In RxJava this is simple using the onError.
Would this make it easier for testing?
Is this the correct use of suspend functions?
Many thanks for any suggestions.
interface PokemonService {
@GET(EndPoints.POKEMON)
suspend fun getPokemons(): PokemonListModel
}
Interactor that will timeout after 10 seconds if the response is too slow or some network error
class PokemonListInteractorImp(private val pokemonService: PokemonService) : PokemonListInteractor {
override suspend fun getListOfPokemons(): PokemonListModel {
return withTimeout(10_000) {
pokemonService.getPokemons()
}
}
}
Inside my view model I use the viewModelScope. Just wondering if I should be catching exceptions.
fun fetchPokemons() {
viewModelScope.launch {
try {
shouldShowLoading.value = true
pokemonListLiveData.value = pokemonListInteractor.getListOfPokemons()
}
catch(error: Exception) {
errorMessage.value = error.localizedMessage
}
finally {
shouldShowLoading.value = false
}
}
}
In my fragment I am just observing the live data and populating the adapter.
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
bindings = FragmentPokemonListBinding.inflate(inflater, container, false)
setupAdapter()
pokemonViewModel.registerPokemonList().observe(viewLifecycleOwner, Observer { pokemonList ->
pokemonAdapter.populatePokemons(pokemonList.pokemonList)
})
return bindings.root
}
I suggest to use sealed
Result
class and try/catch
block to handle api response exceptions:
sealed class Result<out T : Any>
class Success<out T : Any>(val data: T) : Result<T>()
class Error(val exception: Throwable, val message: String = exception.localizedMessage) : Result<Nothing>()
inline fun <T : Any> Result<T>.onSuccess(action: (T) -> Unit): Result<T> {
if (this is Success) action(data)
return this
}
inline fun <T : Any> Result<T>.onError(action: (Error) -> Unit): Result<T> {
if (this is Error) action(this)
return this
}
Catch exceptions in PokemonListInteractorImp
using try/catch
block and return appropriate Result
:
class PokemonListInteractorImp(private val pokemonService: PokemonService) : PokemonListInteractor {
override suspend fun getListOfPokemons(): Result<PokemonListModel> {
return withTimeout(10_000) {
try {
Success(pokemonService.getPokemons())
} catch (e: Exception) {
Error(e)
}
}
}
}
In your ViewModel
you can use extension functions onSuccess
, onError
on Result
object to handle the result:
fun fetchPokemons() = viewModelScope.launch {
shouldShowLoading.value = true
pokemonListInteractor.getListOfPokemons()
.onSuccess { pokemonListLiveData.value = it }
.onError { errorMessage.value = it.message }
shouldShowLoading.value = false
}
As you use launch
coroutine builder, it bubbles up the exceptions. So I think CoroutineExceptionHandler
will be an alternative way of handling uncaught exceptions in a more idiomatic way. The advantages are
Take a look at this example; I have tried to showcase a few scenarios;
/**
* I have injected coroutineScope and the coroutineExceptionHandler in the constructor to make this class
* testable. You can easily mock/stub these in tests.
*/
class ExampleWithExceptionHandler(
private val coroutineScope: CoroutineScope = CoroutineScope(
Executors.newFixedThreadPool(2).asCoroutineDispatcher()
),
private val coroutineExceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
println(
"Exception Handler caught $throwable, ${throwable.suppressed}" //you can get the suppressed exception, if there's any.
)
}
) {
/**
* launch a coroutine with an exception handler to capture any exception thrown inside the scope.
*/
fun doWork(fail: Boolean): Job = coroutineScope.launch(coroutineExceptionHandler) {
if (fail) throw RuntimeException("an error...!")
}
}
object Runner {
@JvmStatic
fun main(args: Array<String>) {
val exampleWithExceptionHandler = ExampleWithExceptionHandler()
//a valid division, all good. coroutine completes successfully.
runBlocking {
println("I am before doWork(fail=false)")
exampleWithExceptionHandler.doWork(false).join()
println("I am after doWork(fail=false)")
}
//an invalid division. Boom, exception handler will catch it.
runBlocking {
println("I am before doWork(fail=true)")
exampleWithExceptionHandler.doWork(true).join()
println("I am after doWork(fail=true)")
}
println("I am on main")
}
}
Output
I am before doWork(fail=false)
I am after doWork(fail=false)
I am before doWork(fail=true)
Exception Handler caught java.lang.RuntimeException: an error...!, [Ljava.lang.Throwable;@53cfcb7a
I am after doWork(fail=true)
I am on main
You can see the exception has been captured by the handler successfully. If coroutines are nested, you can get the inner exception with suppressed
method.
This approach is good for non-async coroutines. The async
coroutines are a different beast. If you try to await
on an async
coroutine inside the same runBlocking
code, the exceptions won't be handled propagated like launch
type. It will still throw out of the scope and kills the main thread. For async, I saw that you can use supervisorScope
or wrapped coroutine (which I haven't got a chance to use).
As propagated unhandled exceptions can be handled globally. This style can help you with reuse of exception handler code and any subsequent operations. For example, the docs suggest;
Normally, the handler is used to log the exception, show some kind of error message, terminate, and/or restart the application.
A similar approach can be found when you use Spring framework with global exception handlers.
Possible drawbacks are;
About suspension, If your API/functions are fully async, you can return the Job
or Deferred<T>
created by the coroutine scope. Otherwise, you have to block somewhere in your code to complete the coroutine and return the value.
This doc is very useful https://kotlinlang.org/docs/reference/coroutines/exception-handling.html
Another good resource specific to Android apps - https://alexsaveau.dev/blog/kotlin/android/2018/10/30/advanced-kotlin-coroutines-tips-and-tricks/#article
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