Kotlin协程如何处理错误以及实现方法

12

第一次使用协程。需要帮助。

这是我的流程:

Presenter想要登录,所以调用Repository接口。Repository实现了RepositoryInterface。因此,Repository调用APIInterface。APIInterface由APIInterfaceImpl实现。最终,APIInterfaceImpl调用MyRetrofitInterface。

以下是逻辑图:

Presenter -> Repository -> APIInterfaceImpl -> MyRetrofitInterface

一旦我获得登录响应:

APIInterfaceImpl -> Repository -> 将数据存储在缓存中 -> 将http状态码提供给Presenter

以下是我的代码:

RepositoryInterface.kt

fun onUserLogin(loginRequest: LoginRequest): LoginResponse

代码库.kt

class Repository : RepositoryInterface {
   private var apiInterface: APIInterface? = null

   override fun onUserLogin(loginRequest: LoginRequest): LoginResponse {
         return apiInterface?.makeLoginCall(loginRequest)
   }
}

API接口.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>?

我的问题是:

  1. 我的架构是否正确?
  2. 如何在代码中传递HTTP错误代码或者没有网络连接的情况?
  3. 有没有更好的解决方案?

你在哪里以及如何启动协程? - Sergio
是的,那就是我的问题... 你能告诉我在哪里以及如何做吗? - Alessandra Maria
2个回答

15

在本地范围内启动协程是一个好的实践,它可以在生命周期感知类中实现,例如PresenterViewModel。您可以使用下面的方法传递数据:

  1. 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>()
    
  2. Make onUserLogin function suspendable and returning Result in RepositoryInterface and Repository:

    suspend fun onUserLogin(loginRequest: LoginRequest): Result<LoginResponse> {
        return apiInterface.makeLoginCall(loginRequest)
    }
    
  3. 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))
        }
    }
    
  4. 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 */ }
            }
        }
    }
    

编辑

我们可以在Result类上使用扩展函数来替换when表达式:

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 */ }
    }
}

谢谢您的示例...您能展示一下仓库的代码吗?MyRetrofitInterface应该返回Deferred还是Result? - Alessandra Maria
谢谢...在我的代码库中,我有一个问题:我需要存储apiInterface.makeLoginCall(loginRequest)的主体内容。由于它返回结果,我该如何获取其主体内容? - Alessandra Maria
你可以这样做:if (result is Success<LoginResponse>) { val response = result.data } - Sergio
makeLoginCall 函数中,有一个对 MyRetrofitInterface?.loginRequest(loginRequest)?.await() 的调用,因此它已经在另一个线程上运行,不需要使用 withContext(Dispatchers.IO)MyRetrofitInterface?.loginRequest(loginRequest)? 返回一个 Deferred 对象,通常与 await 暂停函数一起使用,以便等待结果而不阻塞 主/当前线程。@NimrodDayan 我稍微编辑了一下评论,以避免误解。 - Sergio
Deferred 类型及其 await() 方法用于并发。它本身不提供任何“等待结果而不阻塞”的方法。例如,如果在主调度程序的上下文中运行 async { Thread.sleep(5000L) },将会阻塞主线程!你代码中的 repo.onUserLogin(request) 之所以不会阻塞主线程,是因为 Retrofit 在内部管理了工作线程来处理排队的请求。但这是 Retrofit 的实现细节。这并不意味着返回 Deferred 的任何 API 都会做同样的事情,这就是强调的重点。 - Nimrod Dayan
显示剩余5条评论

6

编辑:

我正在尝试在我的新应用程序中使用此解决方案,并且发现如果在launchSafe方法中发生错误并尝试重试请求,则launcSafe()方法不会正确工作。因此,我将逻辑更改为以下内容,问题得到了解决。

fun CoroutineScope.launchSafe(
    onError: (Throwable) -> Unit = {},
    onSuccess: suspend () -> Unit
) {
   launch {
        try {
            onSuccess()
        } catch (e: Exception) {
            onError(e)
        }
    }
}

新答案:

我对这个话题思考了很多,并得出了一个解决方案。我认为这个方案更加清晰,易于处理异常。首先,在编写代码时,应该像下面这样:

fun getNames() = launch { }  

您正在将作业实例返回给UI,我认为这是不正确的。UI不应该引用作业实例。我尝试了以下解决方案,对我来说效果很好。但我想讨论是否会出现任何副作用。期待您的评论。
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()
    }
}

你已经从 CoroutineExceptionHandler 转向使用 try-catch 了吗? - CoolMind
1
是的,我已经移动了,你肯定应该移动。因为第一种方法并不是最好的。 - toffor
1
谢谢!我已经使用 try-catch 两年了。我认为 CoroutineExceptionHandler 会更好。 - CoolMind
这是一个不错的变体,但我发现我们无法获取正确的类名、方法名和调用方法的行号。在 try-catch 中,我们可以调用 Thread.currentThread().stackTrace[2] 并获取崩溃行的行号,但不能获取调用类的行号,而是写有 CoroutineScope.launchSafe 的类。我的意思是,如果我们在 MyLaunch.kt 中编写此扩展,则在 try-catch 中我们将得到 MyLaunch.launchSafe:10,而不是 SomeFragment.loadItems:120 - CoolMind
为了克服这种行为,您可以使用inline修饰符与crossinlinenoinline修饰符。在这种情况下,我们可以捕获调用方法,但不能捕获其行号。 - CoolMind
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接