【问题标题】:Using try catch block in swallowing exceptions when using kotlin coroutines使用 kotlin 协程时使用 try catch 块吞下异常
【发布时间】:2020-11-15 10:25:33
【问题描述】:
kotlin coroutines version 1.3.8
kotlin 1.3.72

这是我第一次使用协程,并且我已经使用协程转换了我的 rxjava2。但由于这是我第一次想知道我是否遵循最佳做法。

  1. 我的一个问题是捕获异常,因为在 kotlin 中这可能是一种不好的做法,因为吞下异常可能会隐藏一个严重的错误。但是使用协程还有其他方法可以捕获错误。在 RxJava 中,使用 onError 很简单。

  2. 这会让测试更容易吗?

  3. 这是对挂起函数的正确使用吗?

非常感谢您的任何建议。

interface PokemonService {
    @GET(EndPoints.POKEMON)
    suspend fun getPokemons(): PokemonListModel
}

如果响应太慢或某些网络错误,将在 10 秒后超时的交互器

class PokemonListInteractorImp(private val pokemonService: PokemonService) : PokemonListInteractor {
    override suspend fun getListOfPokemons(): PokemonListModel {
        return withTimeout(10_000) {
            pokemonService.getPokemons()
        }
    }
}

在我的视图模型中,我使用 viewModelScope。只是想知道我是否应该捕获异常。

fun fetchPokemons() {
    viewModelScope.launch {
        try {
            shouldShowLoading.value = true
            pokemonListLiveData.value = pokemonListInteractor.getListOfPokemons()
        }
        catch(error: Exception) {
            errorMessage.value = error.localizedMessage
        }
        finally {
            shouldShowLoading.value = false
        }
    }
}

在我的片段中,我只是观察实时数据并填充适配器。

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
   bindings = FragmentPokemonListBinding.inflate(inflater, container, false)

    setupAdapter()
    pokemonViewModel.registerPokemonList().observe(viewLifecycleOwner, Observer { pokemonList ->
        pokemonAdapter.populatePokemons(pokemonList.pokemonList)
    })

    return bindings.root
}

【问题讨论】:

    标签: kotlin try-catch kotlin-coroutines


    【解决方案1】:

    我建议使用sealedResult类和try/catch块来处理api响应异常:

    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>()
    
    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
    }
    

    使用try/catch 块捕获PokemonListInteractorImp 中的异常并返回适当的Result

    class PokemonListInteractorImp(private val pokemonService: PokemonService) : PokemonListInteractor {
        override suspend fun getListOfPokemons(): Result<PokemonListModel> {
            return withTimeout(10_000) {
                try {
                    Success(pokemonService.getPokemons())
                } catch (e: Exception) {
                    Error(e)
                }
            }
        }
    }
    

    在您的ViewModel 中,您可以在Result 对象上使用扩展函数onSuccessonError 来处理结果:

    fun fetchPokemons() = viewModelScope.launch {
        shouldShowLoading.value = true
        pokemonListInteractor.getListOfPokemons()
                .onSuccess { pokemonListLiveData.value = it }
                .onError { errorMessage.value = it.message }
        shouldShowLoading.value = false
    }
    

    【讨论】:

      【解决方案2】:

      当您使用 launch 协程构建器时,它会冒泡异常。所以我认为CoroutineExceptionHandler 将是一种以更惯用的方式处理未捕获异常的替代方法。优点是

      • 协程中抛出的异常不会被吞没,你有更好的可见性
      • 您可以在协程中清晰地测试异常传播和处理(如果您实现了异常处理程序)
      • 您可以减少/避免样板的 try/catch

      看看这个例子;我试图展示一些场景;

      /**
       * I have injected coroutineScope and the coroutineExceptionHandler in the constructor to make this class
       * testable. You can easily mock/stub these in tests.
       */
      class ExampleWithExceptionHandler(
          private val coroutineScope: CoroutineScope = CoroutineScope(
              Executors.newFixedThreadPool(2).asCoroutineDispatcher()
          ),
          private val coroutineExceptionHandler: CoroutineExceptionHandler = CoroutineExceptionHandler { _, throwable ->
              println(
                  "Exception Handler caught $throwable, ${throwable.suppressed}" //you can get the suppressed exception, if there's any.
              )
          }
      ) {
          /**
           * launch a coroutine with an exception handler to capture any exception thrown inside the scope.
           */
          fun doWork(fail: Boolean): Job = coroutineScope.launch(coroutineExceptionHandler) {
              if (fail) throw RuntimeException("an error...!")
          }
      
      }
      
      object Runner {
      
          @JvmStatic
          fun main(args: Array<String>) {
              val exampleWithExceptionHandler = ExampleWithExceptionHandler()
              //a valid division, all good. coroutine completes successfully.
              runBlocking {
                  println("I am before doWork(fail=false)")
                  exampleWithExceptionHandler.doWork(false).join()
                  println("I am after doWork(fail=false)")
              }
              //an invalid division. Boom, exception handler will catch it.
              runBlocking {
                  println("I am before doWork(fail=true)")
                  exampleWithExceptionHandler.doWork(true).join()
                  println("I am after doWork(fail=true)")
              }
      
              println("I am on main")
          }
      }
      

      输出

      I am before doWork(fail=false)
      I am after doWork(fail=false)
      I am before doWork(fail=true)
      Exception Handler caught java.lang.RuntimeException: an error...!, [Ljava.lang.Throwable;@53cfcb7a
      I am after doWork(fail=true)
      I am on main
      

      您可以看到异常已被处理程序成功捕获。如果协程是嵌套的,可以通过suppressed方法获取内部异常。

      这种方法适用于非异步协程。 async 协程是不同的野兽。如果您尝试在同一 runBlocking 代码内的 async 协程上使用 await,则不会像 launch 类型那样处理异常传播。它仍然会抛出范围并杀死主线程。对于异步,我看到您可以使用supervisorScope 或包装的协程(我没有机会使用)。

      由于传播的未处理异常可以全局处理。这种风格可以帮助您重用异常处理程序代码和任何后续操作。例如,文档建议;

      通常,处理程序用于记录异常,显示某种 错误消息、终止和/或重新启动应用程序。

      当您使用带有全局异常处理程序的 Spring 框架时,可以找到类似的方法。

      可能的缺点是;

      • 这仅适用于未捕获的异常,不可恢复
      • 这可能看起来像 AOP 样式代码
      • 根据异常返回不同的值可以将逻辑集中在异常处理程序中。
      • 必须充分了解异常是如何传播的,具体取决于协程构建器和作用域的使用情况

      关于暂停,如果你的 API/函数是完全异步的,你可以返回由协程作用域创建的 JobDeferred&lt;T&gt;。否则,您必须在代码中的某处阻塞才能完成协程并返回值。

      这个文档非常有用https://kotlinlang.org/docs/reference/coroutines/exception-handling.html

      另一个专门针对 Android 应用的好资源 - https://alexsaveau.dev/blog/kotlin/android/2018/10/30/advanced-kotlin-coroutines-tips-and-tricks/#article

      【讨论】:

      • 在文档中针对所描述的情况特别建议不要使用CoroutineExceptionHandler,因为您无法使用它从异常中恢复。它建议改用 try/catch 块。
      • 这个答案针对测试以及如何防止协程中的吞咽异常。我不认为有这样的建议,因为我重新阅读了文档。但它描述了这适用于未捕获的异常。
      • 在处理协程异常部分下检查。我认为提供 CoroutineExceptionHandler 实际上是更多样板,因为每个 Coroutine 都需要一个特定的来处理显示特定消息。 try/catch 会减少行数、可读性和标准。
      • 这里是链接。 kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/…。第二段指出CoroutineExceptionHandler 是最后的机制,应该使用 try/catch 来捕获代码特定部分中的异常,例如 OP 的示例。 Try/catch 更合适,因为它是可组合的,并且可以从中恢复以启动另一个协程。
      • CoroutineExceptionHandler 在 OP 的情况下的预期目的,因此将是 除了 到 try/catch 块,作为仅记录未捕获异常的最后手段。这种方法不适合android。如何从coroutineExceptionHandler 中显示AlertDialog?您必须将context 注入其中。这很丑陋,不利于测试。如果您进行不同的 api 调用引发相同的异常,您如何显示不同的消息?这在 android 上不是惯用的,也是一个坏主意。
      【解决方案3】:

      在您的 PokemonListInteractorImp 类中,处理响应异常并随心所欲地处理它。在 ViewModel 中,您为 List 的某些 LiveData 对象设置值,这应该已经是成功状态。尝试类似:

      protected suspend fun <T> requestApiCall(call: suspend () -> T): Either<FailureState, T> {
              return try {
                  Either.Right(call.invoke())
              } catch (e: HttpException) {
                  return Either.Left(FailureState.ServerError)
              } catch (e: UnknownHostException) {
                  return Either.Left(FailureState.NetworkConnection)
              } catch (e: Throwable) {
                  e.printStackTrace()
                  return Either.Left(FailureState.UnknownError)
              }
          }
      

      故障状态类:

      sealed class FailureState {
          object NetworkConnection : FailureState()
          object ServerError : FailureState()
          object UnknownError : FailureState()
      
          /** * Extend this class for feature specific failures.*/
          abstract class FeatureFailure: FailureState()
      }
      

      ViewModel,类似于:

          fun loadQuestions(type: String) {
                  viewModelScope.launch {
                      questionsUseCase.invoke(type).fold(::handleError, ::handleUsersResponse)
                  }
              }
      
       private fun handleUsersResponse(questionsResponse: QuestionsResponse) {
              questionsResponse.questions?.apply {
                  postScreenState(ShowQuestions(map { it.toDomainModel() }.toMutableList()))
              }
          }
      

      类似的,希望对你有帮助。 但是,如果你只是想在协程中处理异常,这里有一个很好的来源: https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c#:~:text=Coroutines%20use%20the%20regular%20Kotlin,treat%20exceptions%20in%20different%20ways.

      如果你有任何问题,尽管问。

      【讨论】:

        【解决方案4】:

        回答您的问题:

        1. 是的,如果您不希望您的应用在用户关闭 Wifi 时崩溃,您必须捕获网络异常! Rxjava 的 onError lambda 相当于 kotlin 协程中的 try/catch 块(尽管我更喜欢 runCatching {}.onFailure {} 语法糖)。
        2. 你的意思是协程更容易测试 RxJava 吗?我会说它们很相似,但互联网上还没有关于测试协程的那么多信息。
        3. 我看到您使用挂起函数的唯一问题是您在主线程上运行它,见下文:

        这是我将如何编写您的 fetchPokemons 函数:

        fun fetchPokemons() {
            viewModelScope.launch {
                shouldShowLoading.value = true
                runCatching { 
                    // Inject ioDispatcher into this class, so you can replace it with testDispatcher in tests
                    withContext(ioDispatcher) {
                        pokemonListInteractor.getListOfPokemons() // This happens on IO dispatcher
                    }
                }.onSuccess { pokemonList ->
                    pokemonListLiveData.value = pokemonList // This happens on Main (UI) dispatcher
                }.onFailure {
                    errorMessage.value = error.localizedMessage // On Main dispatcher too
                }
                
                // Finally block not needed since this will wait for the suspending function above
                shouldShowLoading.value = false
            }
        }
        

        这是基本方法,但是有充分的理由更进一步,将您的 PokemonListModel 包装在 Result 类型中。 你可以:

        主要优点是它迫使每个使用您的PokemonListInteractor 的人考虑处理错误情况。 Kotlin 没有检查异常使得 Result 类型更加必要,因为使用上述方法很容易丢失需要处理错误的地方。

        【讨论】:

        • 因为 Retrofit 的挂起函数是“主安全”的,所以没有必要切换到 Dispatchers.IO。我在这里写过 => lukaslechner.com/…
        • 有趣。我认为房间也是主要安全的。但是,我宁愿不将我的 viewModel 与存储库使用的特定技术联系起来
        • 我在哪里可以放 finally 代码?喜欢近距离直播?
        猜你喜欢
        • 2018-05-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2011-10-06
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多