在KotlinFlow Android中发出异常

uelo1irk  于 2023-02-02  发布在  Android
关注(0)|答案(3)|浏览(224)

我已经在流内部发出异常,并在异常之下。

IllegalStateException: Flow exception transparency is violated:
    Previous 'emit' call has thrown exception java.lang.NullPointerException, but then emission attempt of value 'planetbeyond.domain.api.Resource$Error@85b4d28' has been detected.
    Emissions from 'catch' blocks are prohibited in order to avoid unspecified behaviour, 'Flow.catch' operator can be used instead.
    For a more detailed explanation, please refer to Flow documentation.
       at kotlinx.coroutines.flow.internal.SafeCollector.exceptionTransparencyViolated(SafeCollector.kt:140)
       at kotlinx.coroutines.flow.internal.SafeCollector.checkContext(SafeCollector.kt:104)
       at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:83)
       at kotlinx.coroutines.flow.internal.SafeCollector.emit(SafeCollector.kt:66)
       at planetbeyond.domain.use_cases.OptionSelectedCountUsecase$invoke$1.invokeSuspend(OptionSelectedCountUsecase.kt:20)

所选选项计数用例.kt

class OptionSelectedCountUsecase @Inject constructor(
private val repository: Repository
) {
    operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
        emit(Resource.Loading())
        try {
            val data = repository.getOptionSelectedCount(questionId)
            emit(Resource.Success(data))
        } catch (e: Exception) {
            emit(Resource.Error(e.toString()))// crashed at this line when api don't response anything or some sort of server error
        }
    }
}

存储库.kt

interface Repository{
  suspend fun getOptionSelectedCount(questionId: Int):List<OptionSelectedCountModel>
}

存储库实施kt

class RepositoryImpl @Inject constructor(
    private val apiService: ApiService
) : Repository {
   override suspend fun getOptionSelectedCount(questionId: Int): List<OptionSelectedCountModel> {
        return apiService.getOptionSelectedCount(questionId).data.map {
            it.toModel()
        }
    }
}

应用程序服务.kt

interface ApiService {
    @GET("get_option_selected_count")
    suspend fun getOptionSelectedCount(
        @Query("question_id") question_id: Int
    ): WebResponse<List<OptionSelectedCountDto>>
}

实时显示问题视图模型.kt

@HiltViewModel
class LiveShowQuestionsViewModel @Inject constructor(
    private val optionSelectedCountUsecase: OptionSelectedCountUsecase
) : ViewModel() { 
   fun getOptionSelectedCount(questionId: Int) {
        optionSelectedCountUsecase(questionId).onEach {
            when (it) {
                is Resource.Loading -> {
                    _optionSelectedCountState.value = OptionSelectedCountState(isLoading = true)
                }
                is Resource.Error -> {
                    _optionSelectedCountState.value = OptionSelectedCountState(error = it.message)
                }
                is Resource.Success -> {
                    _optionSelectedCountState.value = OptionSelectedCountState(data = it.data)
                }
            }
        }///.catch {  } // Why must I have to handle it here 
            .launchIn(viewModelScope)
    }
}

有必要像上面评论的那样在流外处理异常吗?最佳实践是什么?

ewm0tg9j

ewm0tg9j1#

问题是你在try中 Package 了一个emit调用,并试图在匹配的catch块中发出。这意味着如果emit调用本身抛出(这可能是由流的一些下游问题引起的),它将被指示再次发出。这是非常模糊和脆弱的行为。
相反,您可以将emit调用移到try/catch之外:

class OptionSelectedCountUsecase @Inject constructor(
private val repository: Repository
) {
    operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
        emit(Resource.Loading())
        val result = try {
            val data = repository.getOptionSelectedCount(questionId)
            Resource.Success(data)
        } catch (e: Exception) {
            Resource.Error(e.toString())
        }
        emit(result)
    }
}

不知何故,你在你的收集器中引起了一个NullPointerException,这是一个单独的问题需要解决。

ttygqcqt

ttygqcqt2#

根本问题是你的

emit(Resource.Success(data))

抛出一个异常。当你捕捉到这个异常时,你仍然在"emit"块中,你正在尝试

emit(Resource.Error(e.toString())

这就像发射中的发射。所以这是错的。
但是让我们后退一步。为什么在第一次发射时会出现异常?看起来这个数据对象没有正确填充数据,可能是因为你提到的问题(错误响应等),在它到达收集器后会出现空指针异常。
所以基本流程应该是
1.尝试进行调用,并捕获http/parsing异常(如果存在异常)(发射失败)
1.如果没有异常,则验证对象是否包含正确的字段。如果数据不一致,则发出错误
1.如果一切正常,发射成功
例如:

class OptionSelectedCountUsecase @Inject constructor(
private val repository: Repository
) {
    operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
        emit(Resource.Loading())
        try {
            val data = repository.getOptionSelectedCount(questionId)
            if(validateData(data)){
               emit(Resource.Success(data))
            }else{
              // some data integrity issues, missing fields
              emit(Resource.Error("TODO error")
            }
        } catch (e: HttpException) {
            // catch http exception or parsing exception etc
            emit(Resource.Error(e.toString()))
        }
    }
}

理想情况下,应该将其拆分为以下几个部分,以免干扰emit的异常捕获:

class OptionSelectedCountUsecase @Inject constructor(
private val repository: Repository
) {
    operator fun invoke(questionId: Int): Flow<Resource<List<OptionSelectedCountModel>>> = flow {
        emit(Resource.Loading())
        emit(getResult(questionId))
    }

    fun getResult(questionId: Int): Resource<List<OptionSelectedCountModel>>{
       try {
            val data = repository.getOptionSelectedCount(questionId)
            if(validateData(data)){
              return Resource.Success(data)
            }else{
              // some data integrity issues, missing fields
              return Resource.Error("TODO error"
            }
        } catch (e: HttpException) {
            // catch http exception or parsing exception etc
           return Resource.Error(e.toString())
        }
   }

}
hmae6n7t

hmae6n7t3#

您不应该手动发出异常和错误。否则,如果不检查发出的值是否为错误,流的用户将不知道异常是否实际发生。
您希望提供异常透明性,因此最好在收集流时处理它们。
其中一种方法是使用catch操作符,为了简化流收集,我们将捕获行为 Package 在一个函数中。

fun <T> Flow<T>.handleErrors(): Flow<T> = 
    catch { e -> showErrorMessage(e) }

然后,在收集流量时:

optionSelectedCountUsecase(questionId)
    .onEach { ... }
    .handleErrors()
    .launchIn(viewModelScope)

注意,如果你只想处理调用用例时的错误,你可以改变操作符的顺序。之前的顺序也允许你处理onEach块的错误。下面的例子只处理调用用例时的错误。

optionSelectedCountUsecase(questionId)
    .handleErrors()
    .onEach { ... }
    .launchIn(viewModelScope)

Read more about exception handling in flows

相关问题