【问题标题】:Kotlin coroutines handle error and implementationKotlin 协程处理错误和实现
【发布时间】:2019-06-02 07:22:07
【问题描述】:

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

这是我的流程:

Presenter 想要登录所以调用 Repository Interface。 Repository 实现 RepositoryInterface。 所以 Repository 调用 APIInterface。 APIInterface 由 APIInterfaceImpl 实现。 APIInterfaceImpl 最终调用了 MyRetrofitInterface。

这是流程图:

Presenter -> Repository -> APIInterfaceImpl -> MyRetrofitInterface

收到登录响应后:

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

这是我的代码:

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>?

我的问题是:

  1. 我的方法在架构上是否正确?
  2. 如何在我的代码中传递 http 错误代码或没有互联网连接
  3. 我的解决方案有更好的方法吗?

【问题讨论】:

  • 在哪里以及如何启动协程?
  • 是的,这就是我的问题...你能告诉我如何以及在哪里可以做到这一点吗?

标签: android kotlin kotlin-coroutines coroutine


【解决方案1】:

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

  1. 在单独的文件中创建sealed Result 类及其继承者:

    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. 使onUserLogin 函数可暂停并在RepositoryInterfaceRepository 中返回Result

    suspend fun onUserLogin(loginRequest: LoginRequest): Result<LoginResponse> {
        return apiInterface.makeLoginCall(loginRequest)
    }
    
  3. 根据以下代码更改APIInterfaceAPIInterfaceImpl中的makeLoginCall函数:

    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. 为您的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 应该返回什么?延期还是结果?
  • MyRetrofitInterface 与您的代码相同。在我的答案中添加了存储库代码
  • 谢谢...只是我存储库中的最后一个问题,我需要存储 apiInterface.makeLoginCall(loginRequest) 的主体。既然它返回一个结果,我怎么能得到它的主体?
  • 你可以这样做:if (result is Success&lt;LoginResponse&gt;) { val response = result.data }
  • @Sergey login function isn't blocking the Main Thread when it is marked as 'suspend' 这不是真的。挂起函数不会神奇地将阻塞代码变为解除阻塞代码。在您的示例中,对 repo.onUserLogin(request) 的调用将阻塞主线程。为了避免它,您必须使用非主调度程序。由于这是一个 Web 请求,因此推荐的调度程序是 Dispatchers.IO。要更改调度程序,您应该使用val result = withContext(Dispatchers.IO) { repo.onUserLogin(request) } 包装调用。
【解决方案2】:

编辑:

我正在我的新应用程序中尝试此解决方案,并且我发布了如果 launchSafe 方法发生错误并尝试重试请求,则 launcSafe() 方法无法正常工作。所以我改变了这样的逻辑,问题就解决了。

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

旧答案:

我对这个话题想了很多,并提出了一个解决方案。我认为这个解决方案更干净,易于处理异常。首先当使用写代码的时候像

fun getNames() = launch { }  

您正在将作业实例返回给 ui,我认为这是不正确的。 Ui 不应该引用作业实例。我尝试了以下解决方案,它对我很有用。但我想讨论是否会出现任何副作用。很高兴看到你的 cmets。

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了吗?
  • 是的,我搬家了,你应该明确地搬家。因为第一种方法不是最好的。
  • 谢谢!我已经使用try-catch 2 年了。我想,CoroutineExceptionHandler 会更好。
  • 这是一个很好的变体,但是我发现我们无法获得正确的类名、方法名、调用方法的行号。在try-catch 中,我们可以调用Thread.currentThread().stackTrace[2] 并获取崩溃行的行号,但不是调用类的行号,而是写入CoroutineScope.launchSafe 的类。我的意思是,如果我们在MyLaunch.kt 中编写此扩展,然后在try-catch 中,我们将得到MyLaunch.launchSafe:10,而不是SomeFragment.loadItems:120
  • 要克服这种行为,您可以使用 inline 修饰符和 crossinlinenoinline 修饰符。在这种情况下,我们可以捕获一个调用方法,但不能捕获它的行号。
猜你喜欢
  • 2022-11-02
  • 1970-01-01
  • 2020-09-25
  • 2019-12-10
  • 1970-01-01
  • 2022-01-18
  • 2021-11-11
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多