Using coroutines for the first time. Need help.
Here is my flow:
Presenter wants to login so calls Repository Interface. Repository implements RepositoryInte
It is a good practice to launch a coroutine in a local scope which can be implemented in a lifecycle aware classes, for example Presenter or ViewModel. You can use next approach to pass data:
Create sealed
Result
class and its inheritors in separate file:
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>()
Make onUserLogin
function suspendable and returning Result
in RepositoryInterface
and Repository
:
suspend fun onUserLogin(loginRequest: LoginRequest): Result<LoginResponse> {
return apiInterface.makeLoginCall(loginRequest)
}
Change makeLoginCall
function in APIInterface
and APIInterfaceImpl
according to the following code:
suspend fun makeLoginCall(loginRequest: LoginRequest): Result<LoginResponse> {
if (isInternetPresent()) {
try {
val response = MyRetrofitInterface?.loginRequest(loginRequest)?.await()
return Success(response)
} catch (e: Exception) {
return Error(e)
}
} else {
return Error(Exception(Constants.NO_INTERNET))
}
}
Use next code for your Presenter
:
class Presenter(private val repo: RepositoryInterface,
private val uiContext: CoroutineContext = Dispatchers.Main
) : CoroutineScope { // creating local scope
private var job: Job = Job()
// To use Dispatchers.Main (CoroutineDispatcher - runs and schedules coroutines) in Android add
// implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.0.1'
override val coroutineContext: CoroutineContext
get() = uiContext + job
fun detachView() {
// cancel the job when view is detached
job.cancel()
}
fun login() = launch { // launching a coroutine
val request = LoginRequest()
val result = repo.onUserLogin(request) // onUserLogin() function isn't blocking the Main Thread
//use result, make UI updates
when (result) {
is Success<LoginResponse> -> { /* update UI when login success */ }
is Error -> { /* update UI when login error */ }
}
}
}
EDIT
We can use extension functions on Result
class to replace when
expression:
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
}
class Presenter(...) : CoroutineScope {
// ...
fun login() = launch {
val request = LoginRequest()
val result = repo.onUserLogin(request)
result
.onSuccess {/* update UI when login success */ }
.onError { /* update UI when login error */ }
}
}
EDIT:
I am trying this solution in my new app and i released that if an error occurs in launchSafe method and try to retry request, launcSafe() method does not work correctly. So i changed the logic like this and problem is fixed.
fun CoroutineScope.launchSafe(
onError: (Throwable) -> Unit = {},
onSuccess: suspend () -> Unit
) {
launch {
try {
onSuccess()
} catch (e: Exception) {
onError(e)
}
}
}
OLD ANSWER:
I think a lot about this topic and came with a solution. I think this solution cleaner and easy to handle exceptions. First of all when use write code like
fun getNames() = launch { }
You are returning job instance to ui i think this is not correct. Ui should not have reference to job instance. I tried below solution it's working good for me. But i want to discuss if any side effect can occur. Appreciate to see your comments.
fun main() {
Presenter().getNames()
Thread.sleep(1000000)
}
class Presenter(private val repository: Repository = Repository()) : CoroutineScope {
private val job = Job()
override val coroutineContext: CoroutineContext
get() = job + Dispatchers.Default // Can be Dispatchers.Main in Android
fun getNames() = launchSafe(::handleLoginError) {
println(repository.getNames())
}
private fun handleLoginError(throwable: Throwable) {
println(throwable)
}
fun detach() = this.cancel()
}
class Repository {
suspend fun getNames() = suspendCancellableCoroutine<List<String>> {
val timer = Timer()
it.invokeOnCancellation {
timer.cancel()
}
timer.schedule(timerTask {
it.resumeWithException(IllegalArgumentException())
//it.resume(listOf("a", "b", "c", "d"))
}, 500)
}
}
fun CoroutineScope.launchSafe(
onError: (Throwable) -> Unit = {},
onSuccess: suspend () -> Unit
) {
val handler = CoroutineExceptionHandler { _, throwable ->
onError(throwable)
}
launch(handler) {
onSuccess()
}
}