Home > Software engineering >  Emit Exception in Kotlin Flow Android
Emit Exception in Kotlin Flow Android

Time:02-01

I have emit exception inside flow and got below exception.

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)

OptionSelectedCountUsecase.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
        }
    }
}

Repository.kt

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

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

ApiService.kt

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

LiveShowQuestionViewModel.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)
    }
}

Is it neccessary to handle exception outside flow like commented above. What is the best practice.

CodePudding user response:

You should not emit exceptions and errors manually. Otherwise the user of the flow will not know, if exception actually happened, without checking the emitted value for being an error.

You want to provide exception transparency, therefore it is better to process them on collecting the flow.

One of the ways is to use catch operator. To simplify flow collecting we will wrap the catching behavior in a function.

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

Then, while collecting the flow:

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

Note, that if you want to process only the errors from invocation of the use case, you can change the order of operators. The previous order allows you to process errors from onEach block too. Example below will only process errors from use case invocation.

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

Read more about exception handling in flows

CodePudding user response:

The root problem is that your

emit(Resource.Success(data))

throws an exception. When you catch that exception you are still in the "emit" block and you are trying to

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

So it's like emit inside emit. So yes this is wrong.

But let's get a step backward. Why there is an exception during the first emit? It looks like this data object is not properly filled with data, probably because of the issues that you mentioned (bad response etc), after it reaches the collector there is null pointer exception.

So basic flow should be

  1. try to make the call, and catch http/parsing exception if there is one ( emit failure)
  2. If there was no exception, validate if the object contains proper fields. If data is inconsistent emit Error
  3. If everything is ok emit success

for example:

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

This ideally should be split into, to not mess with exception catching of 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())
        }
   }

}
  • Related