使用 kotlin 协程时在吞咽异常时使用 try catch 块

ant*_*009 5 kotlin kotlin-coroutines

kotlin coroutines version 1.3.8
kotlin 1.3.72
Run Code Online (Sandbox Code Playgroud)

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

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

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

  3. 这是挂起功能的正确使用吗?

非常感谢您的任何建议。

interface PokemonService {
    @GET(EndPoints.POKEMON)
    suspend fun getPokemons(): PokemonListModel
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

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
}
Run Code Online (Sandbox Code Playgroud)

Ser*_*gey 6

我建议使用sealed Resultclass 和try/catchblock 来处理 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
}
Run Code Online (Sandbox Code Playgroud)

PokemonListInteractorImpusingtry/catch块中捕获异常并返回适当的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)
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

ViewModel你可以使用扩展函数onSuccessonErrorResult对象上处理结果:

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


Lak*_*gha 5

当您使用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")
    }
}
Run Code Online (Sandbox Code Playgroud)

输出

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
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

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

可能的缺点是;

  • 这仅适用于未捕获的异常并且不可恢复
  • 这可能看起来像 AOP 风格的代码
  • 根据异常返回不同的值可以将逻辑集中在异常处理程序中。
  • 必须很好地了解如何根据协程构建器和作用域使用异常传播

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

这个文档非常有用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


DM *_*ing 2

在您的 PokemonListInteractorImp 类中,处理响应异常并对其执行任何您想要的操作。在 ViewModel 中,您为列表中的某些 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)
        }
    }
Run Code Online (Sandbox Code Playgroud)

故障状态类别:

sealed class FailureState {
    object NetworkConnection : FailureState()
    object ServerError : FailureState()
    object UnknownError : FailureState()

    /** * Extend this class for feature specific failures.*/
    abstract class FeatureFailure: FailureState()
}
Run Code Online (Sandbox Code Playgroud)

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()))
        }
    }
Run Code Online (Sandbox Code Playgroud)

类似的事情,希望有帮助。但是,如果您只是想处理协程中的异常,这里是很好的来源: https://medium.com/androiddevelopers/exceptions-in-coroutines-ce8da1ec060c# :~:text=Coroutines%20use%20the%20regular%20Kotlin ,以%20不同%20种方式对待%20异常%20。

如果您有任何疑问,请尽管询问。