问题
Using coroutines for the first time. Need help.
Here is my flow:
Presenter wants to login so calls Repository Interface. Repository implements RepositoryInterface. So Repository calls APIInterface. APIInterface is implemented by APIInterfaceImpl. The APIInterfaceImpl finally calls the MyRetrofitInterface.
Here is the flow diagrammatically:
Presenter -> Repository -> APIInterfaceImpl -> MyRetrofitInterface
Once I get login response:
APIInterfaceImpl -> Repository -> Stores the data in cache -> Gives http status code to Presenter
Here is my code:
RepositoryInterface.kt
fun onUserLogin(loginRequest: LoginRequest): LoginResponse
Repository.kt
class Repository : RepositoryInterface {
private var apiInterface: APIInterface? = null
override fun onUserLogin(loginRequest: LoginRequest): LoginResponse {
return apiInterface?.makeLoginCall(loginRequest)
}
}
APIInterface.kt
suspend fun makeLoginCall(loginRequest): LoginResponse?
APIInterfaceImpl.kt
override suspend fun makeLoginCall(loginRequest: LoginRequest): LoginResponse? {
if (isInternetPresent(context)) {
try {
val response = MyRetrofitInterface?.loginRequest(loginRequest)?.await()
return response
} catch (e: Exception) {
//How do i return a status code here
}
} else {
//How do i return no internet here
return Exception(Constants.NO_INTERNET)
}
}
MyRetrofitInterface.kt
@POST("login/....")
fun loginRequest(@Body loginRequest: LoginRequest): Deferred<LoginResponse>?
My questions are:
- Is my approach architecturally right?
- How do I pass http error codes or no internet connection in my code
- Any more nicer approach to my solution?
回答1:
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 returningResult
inRepositoryInterface
andRepository
:suspend fun onUserLogin(loginRequest: LoginRequest): Result<LoginResponse> { return apiInterface.makeLoginCall(loginRequest) }
Change
makeLoginCall
function inAPIInterface
andAPIInterfaceImpl
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 */ }
}
}
回答2:
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()
}
}
来源:https://stackoverflow.com/questions/54077592/kotlin-coroutines-handle-error-and-implementation