协程和改造,处理错误的最佳方法

时间:2019-09-24 15:08:25

标签: android kotlin retrofit2 kotlin-coroutines kotlin-android-extensions

在阅读了此问题How to deal with exception和此媒介Android Networking in 2019 — Retrofit with Kotlin’s Coroutines之后,我创建了一个解决方案,其中包含一个BaseService,该解决方案可以进行改造调用并在“链”:

API

@GET("...")
suspend fun fetchMyObject(): Response<List<MyObject>>

BaseService

protected suspend fun <T : Any> apiCall(call: suspend () -> Response<T>): Result<T> {
    val response: Response<T>
    try {
        response = call.invoke()
    } catch (t: Throwable) {
        return Result.Error(mapNetworkThrowable(t))
    }
    if (!response.isSuccessful) {
        val responseErrorBody = response.errorBody()
        if (responseErrorBody != null) {
            //try to parse to a custom ErrorObject
            ...
            return Result.Error...
        }
        return Result.Error(mapHttpThrowable(Exception(), response.raw().code, response.raw().message))
    }
    return Result.Success(response.body()!!)
}

ChildService

suspend fun fetchMyObject(): Result<List<MyObject>> {
    return apiCall(call = { api.fetchMyObject() })
}

回购

    suspend fun myObjectList(): List<MyObject> {
        return withContext(Dispatchers.IO) {
            when (val result = service.fetchMyObject()) {
                is Result.Success -> result.data
                is Result.Error -> throw result.exception
            }
        }
    }

BaseViewModel

protected suspend fun <T : Any> safeCall(call: suspend () -> T): T? {
    try {
        return call()
    } catch (e: Throwable) {
        parseError(e)
    }
    return null
}

ChildViewModel

fun fetchMyObjectList() {
    viewModelScope.launch {
        safeCall(call = {
            repo.myObjectList()
            //update ui, etc..
        })
    }
}

我认为ViewModel(或BaseViewModel)应该是处理异常的层,因为UI决策逻辑位于该层中,例如,如果我们只想举杯,忽略异常类型,调用另一个函数,等等...

你怎么看?

3 个答案:

答案 0 :(得分:1)

  

我认为ViewModel(或BaseViewModel)应该是该层   处理异常,因为UI决策位于这一层   逻辑,例如,如果我们只想显示敬酒,请忽略   异常,请调用其他函数,等等...

     

你怎么看?

当然,您是正确的。即使逻辑在ViewModel / Repository中,协程也应该在Service上触发。这就是为什么Google已经创建了一个特殊的coroutineScope称为viewModelScope的原因,否则它将是“ repositoryScope”。协程在异常处理方面也有一个不错的功能,称为CoroutineExceptionHandler。这是事情变得更好的地方,因为您不必实现try{}catch{}块:

val coroutineExceptionHanlder = CoroutineExceptionHandler{_, throwable -> 
    throwable.printStackTrance()
    toastLiveData.value = showToastValueWhatever()
}

后来ViewModel

coroutineScope.launch(Dispatchers.IO + coroutineExceptionHanlder){
      val data = serviceOrRepo.getData()
}

当然,您仍然可以在没有try/catch的情况下使用CoroutineExceptionHandler块,可以自由选择。

请注意,在进行Retrofit的情况下,您不需要Dispatchers.IO调度程序,因为Retrofit为您做了(自Retrofit 2.6.0起)。

无论如何,我不能说这篇文章不好,但这并不是最好的解决方案。如果要遵循文章的指南,则可能要检查Transformations on the LiveData

编辑: 您需要进一步了解的是,协程不安全。我的意思是,它们可能会导致内存泄漏,尤其是在整个Android生命周期中。您需要一种在Activity / Fragment不再存在时取消协程的方法。由于ViewModel具有onCleared(在Activity / Fragment被销毁时会被调用),这意味着协程应该向其中之一发射。也许这就是您应该在ViewModel中启动协程的主要原因。请注意,使用viewModelScope并不需要cancel的工作onCleared

一个简单的例子:

viewModelScope.launch(Dispatchers.IO){
   val data = getDataSlowly()
   withContext(Dispatchers.MAIN){
    showData();
  }
} 

或没有viewModelScope的情况:

val job = Job() val coroutineScope = CoroutineContext(Dispatchers.MAIN + job)

fun fetchData(){
  coroutineScope.launch(){
 val data = getDataSlowly()
       withContext(Dispatchers.MAIN){
        showData();
      }
  }
}

//稍后在视图模型中:

override fun onCleared(){
  super.onCleared()
  job.cancel() //to prevent leaks
}

否则,您的Service / Repository将会泄漏。在这种情况下,另一个注意事项是 to use the GlobalScope

答案 1 :(得分:0)

我建议处理存储库中的异常,并将单个响应以密封类对象的形式返回给viewmodel。将其保留在存储库中将使存储库成为事实的单一来源,而您的代码将更多 干净且可读。

答案 2 :(得分:0)

作为对@coroutineDispatcher 优秀答案的补充,我建议捕获异常并检查它是 HttpException、UnknownHostException 还是 SocketException,以便为用户提供有关异常的更多反馈。例如

➜  Projeto npm install webpack webpack-cli webpack-dev-server --save-dev
npm WARN deprecated chokidar@2.1.8: Chokidar 2 will break on node v14+. Upgrade to chokidar 3 with 15x less dependencies.
npm WARN deprecated fsevents@1.2.13: fsevents 1 will break on node v14+ and could be using insecure binaries. Upgrade to fsevents 2.
npm WARN deprecated resolve-url@0.2.1: https://github.com/lydell/resolve-url#deprecated
npm WARN deprecated urix@0.1.0: Please see https://github.com/lydell/urix#deprecated
npm WARN optional SKIPPING OPTIONAL DEPENDENCY: fsevents@^1.2.7 (node_modules/chokidar/node_modules/fsevents):
npm WARN notsup SKIPPING OPTIONAL DEPENDENCY: Unsupported platform for fsevents@1.2.13: wanted {"os":"darwin","arch":"any"} (current: {"os":"linux","arch":"x64"})
npm WARN projeto@1.0.0 No description
npm WARN projeto@1.0.0 No repository field.

+ webpack-dev-server@3.11.2
+ webpack-cli@4.7.0
+ webpack@5.38.1
updated 3 packages and audited 499 packages in 35.305s

1 package is looking for funding
  run `npm fund` for details

found 1 moderate severity vulnerability
  run `npm audit fix` to fix them, or `npm audit` for details
➜  Projeto material-ui
zsh: command not found: material-ui
➜  Projeto npm install @ material-ui / core
npm WARN deprecated material-ui@0.20.2: You can now upgrade to @material-ui/core
npm ERR! code EINVALIDTAGNAME
npm ERR! Invalid tag name "@": Tags may not have any characters that encodeURIComponent encodes.

npm ERR! A complete log of this run can be found in:
npm ERR!     /home/peter/.npm/_logs/2021-06-02T04_35_32_632Z-debug.log

以上所有代码都在if (throwable is SocketException) { //very bad internet return } if (throwable is HttpException) { // parse error body message from //throwable.response().errorBody() return } if (throwable is UnknownHostException) { // probably no internet or your base url is wrong return } ShowSomethingWentWrong()