Kotlin 协程中的异常处理
协程是我们在 Android 中使用 Kotlin 进行异步编程的一种新方式。在开发生产就绪应用程序时,我们希望确保正确处理所有异常,以便用户在使用我们的应用程序时获得愉快的体验。在本文中,我们将讨论在使用 Kotlin 协程时如何正确处理 Android 项目中的异常。如果你想学习如何使用 Kotlin 协程,可以从这里开始。本文将分为以下几个部分:
- 有哪些例外?
- 我们一般如何处理异常?
- 我们如何有效地处理 Kotlin Coroutines 中的异常?
有哪些例外?
异常是在程序运行或执行时发生的意外事件。执行被异常中断,应用程序的预期流程没有执行。这就是为什么为了执行应用程序的正确流程,我们必须在代码中处理异常。
我们一般如何处理异常?
try-catch 块是在 Kotlin 中处理异常的一般方法。在 try 块中,我们编写可能会抛出异常的代码,如果产生异常,则在 catch 块中捕获该异常。让我们用一个例子来说明:
Kotlin
try {
val gfgAnswer = 5 / 0
val addition = 2 + 5
Log.d("GfGMainActivity", gfgAnswer.toString())
Log.d("GfGMainActivity", addition.toString())
} catch (e: Exception) {
Log.e("GfGMainActivity", e.toString())
}
Kotlin
interface GfgService {
@GET("courses")
suspend fun getCourses(): List
@GET("more-courses")
suspend fun getMoreCourses(): List
@GET("error")
suspend fun getCoursesWithError(): List
}
Kotlin
class TryCatchViewModel(
private val gfgUser: GfgUser,
private val gfgCoursedb: DatabaseHelper
) : ViewModel() {
private val gfg = MutableLiveData>>()
fun fetchGfg() {
viewModelScope.launch {
gfg.postValue(Resource.loading(null))
try {
val gfgFromApi = gfgUser.getGfg()
gfg.postValue(Resource.success(gfgFromApi))
} catch (e: Exception) {
gfg.postValue(Resource.error("Something Went Wrong", null))
}
}
}
fun getGfg(): LiveData>> {
return gfg
}
}
Kotlin
fun gfgGfgPro() {
viewModelScope.launch {
gfgPro.postValue(Resource.loading(null))
try {
val moreGfgProFromApi = apiHelper.getGfgProWithError()
val gfgProFromApi = apiHelper.getGfgPro()
val allGfgProFromApi = mutableListOf()
allGfgProFromApi.addAll(gfgProFromApi)
allGfgProFromApi.addAll(moreGfgProFromApi)
gfgPro.postValue(Resource.success(allGfgProFromApi))
} catch (e: Exception) {
gfgPro.postValue(Resource.error("Aw Snap!", null))
}
}
}
Kotlin
fun fetchGfgPro() {
viewModelScope.launch {
gfgPro.postValue(Resource.loading(null))
try {
val moreGfgProFromApi = try {
apiHelper.getGfgProWithError()
} catch (e: Exception) {
emptyList()
}
val gfgProFromApi = try {
apiHelper.getGfgPro()
} catch (e: Exception) {
emptyList()
}
val allGfgProFromApi = mutableListOf()
allGfgProFromApi.addAll(gfgProFromApi)
allGfgProFromApi.addAll(moreGfgProFromApi)
gfgPro.postValue(Resource.success(allGfgProFromApi))
} catch (e: Exception) {
gfgPro.postValue(Resource.error("Aw Snap", null))
}
}
}
Kotlin
class ExceptionHandlerViewModel(
private val gfgServerAPI: GfgServerAPI,
private val gfgHelperDB: DatabaseHelper
) : ViewModel() {
private val gfgUsers = MutableLiveData>>()
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
gfgUsers.postValue(Resource.error("Aw Snap!", null))
}
fun fetchGfgUsers() {
viewModelScope.launch(exceptionHandler) {
gfgUsers.postValue(Resource.loading(null))
val gfgUsersFromApi = gfgServerAPI.getGfgUsers()
gfgUsers.postValue(Resource.success(gfgUsersFromApi))
}
}
fun getGfgUsers(): LiveData>> {
return gfgUsers
}
}
Kotlin
private fun fetchGfgUsers() {
viewModelScope.launch {
gfgUsers.postValue(Resource.loading(null))
try {
val gfgUsersWithErrorFromGfgServerDeferred = async { gfgServerHelper.getGfgUsersWithError() }
val moreGfgUsersFromGfgServerDeferred = async { gfgServerHelper.getMoreGfgUsers() }
val gfgUsersWithErrorFromGfgServer = gfgUsersWithErrorFromGfgServerDeferred.await()
val moreGfgUsersFromGfgServer = moreGfgUsersFromGfgServerDeferred.await()
val allGfgUsersFromGfgServer = mutableListOf()
allGfgUsersFromGfgServer.addAll(gfgUsersWithErrorFromGfgServer)
allGfgUsersFromGfgServer.addAll(moreGfgUsersFromGfgServer)
gfgUsers.postValue(Resource.success(allGfgUsersFromGfgServer))
} catch (e: Exception) {
gfgUsers.postValue(Resource.error(Aw Snap!", null))
}
}
}
在前面的代码中,我们尝试将 5 除以 0,同时将两个数字 2,5 相加。然后应该在 Logcat 中打印解决方案。当我们运行应用程序时,我们应该首先获取解决方案变量中的值,然后将总和分配给附加变量。稍后将在 Log 语句中打印这些值。在这种情况下,不会打印解和加法变量,但 catch 块中的 Log 语句带有算术异常。这样做的原因是没有数字可以被0除。所以,当我们得到异常时,你可以看到第一行以下没有任何步骤,它直接进入了catch块。前面的代码演示了异常是如何发生的以及我们如何处理它。
我们如何有效地处理 Kotlin Coroutines 中的异常?
现在我们将看看如何使用 Kotlin Coroutines 在我们的项目中有效地处理异常。有几种处理异常的方法。
通用方法
使用 SupervisorScope 和 CoroutineExceptionHandler。为了进一步说明这一点,请考虑检索用户列表。我们会有一个界面:
科特林
interface GfgService {
@GET("courses")
suspend fun getCourses(): List
@GET("more-courses")
suspend fun getMoreCourses(): List
@GET("error")
suspend fun getCoursesWithError(): List
}
我们在这里有三个不同的挂起函数,可以用来获取用户列表。如果仔细观察,您会注意到只有前两个函数 getUsers() 和 getMoreUsers() 会返回一个列表,而第三个函数getUserWithError() 会抛出异常。
gfg.HttpException: HTTP 404 Not Found
为了清楚起见,我们特意设计了 getUserWithError() 来抛出异常。现在,让我们回顾一下如何使用 Kotlin 协程正确处理代码中的异常。
1. 通用方法
考虑以下场景:我们有一个 ViewModel,TryCatchViewModel(存在于项目中),我想在 ViewModel 中执行我的 API 调用。
科特林
class TryCatchViewModel(
private val gfgUser: GfgUser,
private val gfgCoursedb: DatabaseHelper
) : ViewModel() {
private val gfg = MutableLiveData>>()
fun fetchGfg() {
viewModelScope.launch {
gfg.postValue(Resource.loading(null))
try {
val gfgFromApi = gfgUser.getGfg()
gfg.postValue(Resource.success(gfgFromApi))
} catch (e: Exception) {
gfg.postValue(Resource.error("Something Went Wrong", null))
}
}
}
fun getGfg(): LiveData>> {
return gfg
}
}
无一例外,这将返回我活动中的用户列表。假设我们向 fetchUsers()函数添加了一个异常。我们将代码更改如下:
科特林
fun gfgGfgPro() {
viewModelScope.launch {
gfgPro.postValue(Resource.loading(null))
try {
val moreGfgProFromApi = apiHelper.getGfgProWithError()
val gfgProFromApi = apiHelper.getGfgPro()
val allGfgProFromApi = mutableListOf()
allGfgProFromApi.addAll(gfgProFromApi)
allGfgProFromApi.addAll(moreGfgProFromApi)
gfgPro.postValue(Resource.success(allGfgProFromApi))
} catch (e: Exception) {
gfgPro.postValue(Resource.error("Aw Snap!", null))
}
}
}
无一例外,这将返回我活动中的用户列表。假设我们向 fetchUsers()函数添加了一个异常。我们将代码更改如下:
科特林
fun fetchGfgPro() {
viewModelScope.launch {
gfgPro.postValue(Resource.loading(null))
try {
val moreGfgProFromApi = try {
apiHelper.getGfgProWithError()
} catch (e: Exception) {
emptyList()
}
val gfgProFromApi = try {
apiHelper.getGfgPro()
} catch (e: Exception) {
emptyList()
}
val allGfgProFromApi = mutableListOf()
allGfgProFromApi.addAll(gfgProFromApi)
allGfgProFromApi.addAll(moreGfgProFromApi)
gfgPro.postValue(Resource.success(allGfgProFromApi))
} catch (e: Exception) {
gfgPro.postValue(Resource.error("Aw Snap", null))
}
}
}
在这种情况下,我们为两个 API 调用添加了一个单独的异常,因此如果发生异常,则会将一个空列表分配给变量并继续执行。
这是以顺序方式执行任务的示例。
2. CoroutineExceptionHandler 的使用
在前面的示例中,您可以看到我们将代码包含在 try-catch 异常中。但是,在使用协程时,我们可以使用名为 CoroutineExceptionHandler 的全局协程异常处理程序来处理异常。
科特林
class ExceptionHandlerViewModel(
private val gfgServerAPI: GfgServerAPI,
private val gfgHelperDB: DatabaseHelper
) : ViewModel() {
private val gfgUsers = MutableLiveData>>()
private val exceptionHandler = CoroutineExceptionHandler { _, exception ->
gfgUsers.postValue(Resource.error("Aw Snap!", null))
}
fun fetchGfgUsers() {
viewModelScope.launch(exceptionHandler) {
gfgUsers.postValue(Resource.loading(null))
val gfgUsersFromApi = gfgServerAPI.getGfgUsers()
gfgUsers.postValue(Resource.success(gfgUsersFromApi))
}
}
fun getGfgUsers(): LiveData>> {
return gfgUsers
}
}
GeekTip: In this case, we’ve added getUsersWithError(), which will throw an exception and pass the handle to the handler.
注意我们这里没有使用 try-catch 块,异常将由 CoroutineExceptionHandler 处理,它充当协程的全局异常处理程序。
3. 使用 SupervisorScope
我们不希望任务的执行因为异常而终止。但是,到目前为止,我们已经看到,每当遇到异常时,我们的执行都会失败并且任务会终止。我们已经看到了如何保持任务在顺序执行中运行;在本节中,我们将看到如何保持任务在并行执行中运行。因此,在我们开始使用 supervisorScope 之前,让我们首先了解并行执行的问题。假设我们执行并行执行,例如:
科特林
private fun fetchGfgUsers() {
viewModelScope.launch {
gfgUsers.postValue(Resource.loading(null))
try {
val gfgUsersWithErrorFromGfgServerDeferred = async { gfgServerHelper.getGfgUsersWithError() }
val moreGfgUsersFromGfgServerDeferred = async { gfgServerHelper.getMoreGfgUsers() }
val gfgUsersWithErrorFromGfgServer = gfgUsersWithErrorFromGfgServerDeferred.await()
val moreGfgUsersFromGfgServer = moreGfgUsersFromGfgServerDeferred.await()
val allGfgUsersFromGfgServer = mutableListOf()
allGfgUsersFromGfgServer.addAll(gfgUsersWithErrorFromGfgServer)
allGfgUsersFromGfgServer.addAll(moreGfgUsersFromGfgServer)
gfgUsers.postValue(Resource.success(allGfgUsersFromGfgServer))
} catch (e: Exception) {
gfgUsers.postValue(Resource.error(Aw Snap!", null))
}
}
}
在这种情况下调用 getUsersWithError() 时会抛出异常,导致我们的 android 应用程序崩溃并终止我们的任务执行。在这个执行中,我们在 coroutineScope 内执行并行执行,它位于 try 块内。我们会从 getUsersWithError() 收到一个异常,一旦发生异常,执行将停止,执行将继续到 catch 块。
GeekTip: When an exception occurs, the task’s execution will be terminated.
因此,我们可以在任务中使用 supervisorScope 来克服执行失败。在 supervisorScope 中,我们有两个作业同时运行,其中一个会抛出异常但任务仍然会完成。在这种情况下,我们使用异步,它返回一个延迟,稍后将传递结果。所以,当我们使用 await() 获取结果时,我们使用 try-catch 块作为表达式,这样如果发生异常,返回一个空列表,但执行完成,我们得到一个用户列表。
结论
- 在不使用异步的同时,我们可以使用 try-catch 或 CoroutineExceptionHandler 来根据我们的用例实现我们想要的任何东西。
- 在使用 async 时,除了 try-catch 之外,我们还有两个选择:coroutineScope 和 supervisorScope。
- 使用异步时,如果您想在其中一个或多个任务失败时继续执行其他任务,则除了顶级 try-catch 之外,对每个任务使用 supervisorScope 和单独的 try-catch。
- 如果您不想继续执行其他任务(如果其中任何任务失败),请使用 coroutineScope 与顶级 try-catch 和异步。