Cannot emit data using flow or channelFlow on Android - android

I'm trying to implement One Tap, so I have created a function that looks like this:
override suspend fun oneTapSgnInWithGoogle() = flow {
try {
emit(Result.Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
emit(Result.Success(result))
} catch (e: Exception) {
Log.d(TAG, "oneTapSgnInWithGoogle: ${e.message}")
emit(Result.Error(e.message!!))
}
}
If I use flow and try to emit the result, my app crashed with the following message:
Flow exception transparency is violated:
StandaloneCoroutine has completed normally; but then emission attempt of value 'Error(message=StandaloneCoroutine has completed normally)' has been detected.
However, if change the code to:
override suspend fun oneTapSgnInWithGoogle() = channelFlow {
try {
send(Result.Loading)
val result = oneTapClient.beginSignIn(signInRequest).await()
send(Result.Success(result))
} catch (e: Exception) {
Log.d(TAG, "oneTapSgnInWithGoogle: ${e.message}")
send(Result.Error(e.message!!))
}
}
And I use channelFlow and try to send the result, the app isn't crashing but I still get the error message saying:
StandaloneCoroutine has completed normally
How can I emit the result correctly and get rid of this error message?
P.S. In my ViewModel class I use:
fun oneTapSgnInWithGoogle() = liveData(Dispatchers.IO) {
viewModelScope.launch {
repo.oneTapSgnInWithGoogle().collect { result ->
emit(result)
}
}
}

This is not a good practice to launch a coroutine in liveData block. liveData block is a suspend lambda, you can collect values directly in it without launching a coroutine:
fun oneTapSgnInWithGoogle() = liveData(Dispatchers.IO) {
repo.oneTapSgnInWithGoogle().collect { result ->
emit(result)
}
}
In your case liveData block has already finished execution (and corresponding coroutine, in which liveData block is executed) when you try to emit a value to LiveData. The solution above should solve the problem.

Related

Emit Exception in Kotlin Flow Android

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.
The problem is that you wrapped an emit call in try and try to emit in the matching catch block. This means that if the emit call itself throws (which ambiguously could be caused by some downstream problem with the flow) it's being instructing to emit again. This is very ambiguous and fragile behavior.
Instead, you can move your emit call(s) outside the 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)
}
}
Somehow, you're causing a NullPointerException in your collector. That's a separate problem to solve.
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
try to make the call, and catch http/parsing exception if there is one ( emit failure)
If there was no exception, validate if the object contains proper fields. If data is inconsistent emit Error
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())
}
}
}
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

Why does kotlin flow not trigger the transform function after an error is handled

I have the below code in my view model class.
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableStateFlow(0)
val marketState: LiveData<State<Market>> =
retry.flatMapLatest{repo.refreshMarket()}
.map { State.Success(it) as State<T> }
.catch { error -> emit(State.Error(error)) }
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.value++
}
}
MarketRepository.kt:
fun refreshMarket() =
flow { emit(api.getMarkets()) }
.onEach { db.upsert(it) }
.flowOn(dispatchers.IO)
It works fine until a network error occurs in the repository method refreshMarket then when I call the retry() on the view model, it doesn't trigger the flatMapLatest transformer function anymore on the retry MutableStateFlow, why?
Does the flow get complete when it calls a Catch block? how to handle such situation?
You're right, catch won't continue emitting after an exception is caught. As the documentation says, it is conceptually similar to wrapping all the code above it in try. If there is a loop in a traditional try block, it does not continue iterating once something is thrown, for example:
try {
for (i in 1..10) {
if (i == 2) throw RuntimeException()
println(i)
}
} catch (e: RuntimeException) {
println("Error!")
}
In this example, once 2 is encountered, the exception is caught, but code flow does not return to the loop in the try block. You will not see any numbers printed that come after 2.
You can use retryWhen instead of catch to be able to restart the flow. To do it on demand like you want, maybe this strategy could be used (I didn't test it):
class MarketViewModel #Inject constructor(repo: MarketRepository) : ViewModel() {
private val retry = MutableSharedFlow<Unit>()
val marketState: LiveData<State<Market>> =
repo.refreshMarket()
.map { State.Success(it) as State<T> }
.retryWhen { error, _ ->
emit(State.Error(error))
retry.first() // await next value from retry flow
true
}
.stateIn(vmScope, SharingStarted.WhileSubscribed(5000), State.Loading())
.asLiveData()
fun retry() {
retry.tryEmit(Unit)
}
}

Kotlin coroutines - gracefully handling errors from suspend functions

Trying to implement graceful handling of the errors with suspend functions that are called from async methods, How to catch the error thrown by a suspend method.
suspend fun findById(id: Long): User? {
throw Exception("my exception") // intentionally throwing to simulate error situation.
return userModel.findById(id) // IO, may throw an error
}
Caller piece, launching with IO thread
GlobalScope.launch(Dispatchers.IO) {
try {
var userAsync: Deferred<User?>? = null
arguments?.getLong("id")?.let {
userAsync = async { viewModel?.findById(it) } // async for efficiency as i've other async methods too.
}
val data = userAsync?.await()
withContext(Dispatchers.Main) {
user = data // data binding, populating UI fields of user
}
} catch (exception: Exception) {
withContext(Dispatchers.Main) { fault(exception) }
}
}
fault method
private fun fault(exception: Exception) {
Log.d("User", "fault: ${exception.localizedMessage}") // expecting output
}
Currently runtime is crashing, want to implement graceful handling of errors.
Attempt 2
Tried placing try catch within the async block but it didn't like it.
var userAsync: Deferred<UserVO?>? = null
arguments?.getLong("id")?.let {
userAsync = async {
try {
delegate?.findById(it)
} catch (e: Exception) {
print(e)
}
}
}
I would use a CoroutineExceptionHandler to make your coroutines fail gracefully:
1) Define the handler:
val exceptionHandler = CoroutineExceptionHandler { context, error ->
// Do what you want with the error
Log.d(TAG, error)
}
2) Refactor your findById function to be executed within an IO context and make your ui code main safe:
suspend fun findById(id : Int) : User? = withContext(Dispatchers.IO){
when(id){
0 -> throw Exception("not valid")
else -> return#withContext User(id)
}
}
Launch your job within MainScope (and so update the ui), passing exceptionHandler to launch coroutine builder in order to catch the exception:
val exceptionHandler = CoroutineExceptionHandler { _, error ->
// Do what you want with the error
Log.d(TAG, error)
}
MainScope().launch(exceptionHandler) {
val user = delegate?.findById(userId)
user?.let {
Timber.v(it.toString())
}
}

How to handle database call errors using Flows

Usually I'm returning from my dao suspend function:
#Dao
interface DataDao {
#Query("SELECT * FROM data")
fun getAllData(): List<Data>
}
And handle the call within the repository:
class DataRepository(
private val dataDao: DataDao
) {
fun getAllData(): Flow<DataState> = flow {
val cacheResult = safeDatabaseCall(dispatcher = Dispatchers.IO) { dataDao.getAllData() }
//handle cacheResult, convert to DataState, emit DataState values
}.flowOn(Dispatchers.IO)
}
With generic fun:
suspend fun <T> safeDatabaseCall(
dispatcher: CoroutineDispatcher,
cacheCall: suspend () -> T?
): CacheResult<T?> {
return withContext(dispatcher) {
try {
withTimeout(10000L) {
CacheResult.Success(cacheCall.invoke())
}
} catch (t: Throwable) {
when (t) {
is TimeoutCancellationException -> {
CacheResult.Error("Timeout error")
}
else -> {
CacheResult.Error("Unknown error")
}
}
}
}
}
The problem is that I want return fun getAllData(): Flow<List<Data>> instead of fun getAllData(): List<Data> In order to get immediate updates, But if I'm returning Flow from the Dao, I can't handle the call with safe call and catch errors.
I thought about collecting the data, but if i'm collecting the data the call already done without error handling
Basically I need the cache result return CacheResult<Data> and not CacheResult<Flow<Data>>
How can I solve the problem And make a generic safeDatabaseCall while returning Flow from Dao?
So if I understand correctly you just want to handle the query and return of information safely in a flow. My only question is around the types. I can sorta assume Data DataState and CacheResult are not the same types so I use a "magic" function that converts the intermediary values to the correct one. You will need to adjust accordingly
class DataRepository(
private val dataDao: DataDao
) {
fun getAllData(): Flow<DataState> = flow {
val result = safeDatabaseCall(dispatcher = Dispatchers.IO) {
dataDao.getAllData()
}
// Emit the result
emit(result)
}.catch { t : Throwable ->
// Do our transformation like before
val result = when (t) {
is TimeoutCancellationException -> {
CacheResult.Error("Timeout error")
}
else -> {
CacheResult.Error("Unknown error")
}
}
// And because catch is actually extending a FlowCollector
// We can emit the result in the stream
emit(result)
}.map { cacheResult ->
convertToDataOrDataState(cacheResult)
}
You shouldn't need flowOn with a dispatcher here since the work inside this flow doesn't require thread dispatching
to Dispatcher.IO. The code we are putting in our flow, is purely exception handling and invoking a function. The only place that seems to require any manual dispatch changing is, safeDatabaseCall(). I am not familiar with this function but if it does exist and takes a dispatcher for the result of actualing making the db calls on an IO thread, then all should be good without flowOn. Otherwise you will be switching dispatchers from original dispatcher -> IO and then to IO again. It's not much but the extra no-op context switch doesn't add anything other than confusion later on.
The flow itself traps any upstream issues and you then make them part of the resulting flow

Can't catch network error when using coroutine, but the error could be caught in RxJava 2. What did I miss?

I have the following code using Coroutine to perform network fetching on background
try {
networkJob = CoroutineScope(Dispatchers.IO).launch {
val result = fetchOnBackground(searchText)
withContext(Dispatchers.Main) {
showResult("Count is $result")
}
}
} catch (exception: Throwable) {
showResult(exception.localizedMessage)
}
When the network is there, all is good. However, when either the host is not correct, or no network, it crashes. The catch can't catch it.
When I code using RxJava
disposable = Single.just(searchText)
.map{fetchOnBackground(it)}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{ showResult("Count is $it") },
{ showResult(it.localizedMessage) })
All works fine. Even in no network situation, the error is caught in the error callback.
What did I miss in the Coroutine code, that I can't catch the error on my end when using the Coroutine?
Note: the network fetching is using OkHttp.
Seems like I need to put the try-catch within the CouroutineScope(Dispatchers.IO).launch
networkJob = CoroutineScope(Dispatchers.IO).launch {
try {
val result = fetchOnBackground(searchText)
showResult("Count is $result")
} catch (exception: Throwable) {
showResult(exception.localizedMessage)
}
}
And I change my showResult to be a suspend function, so that it could include the withContext(Dispatchers.Main)
private suspend fun showResult(result: String) {
withContext(Dispatchers.Main) {
// Code that show the result
}
}

Categories

Resources