I wrote the following callbackFlow
:
fun getData(id: String) = callbackFlow {
val listener = object : ValueEventListener {
// if I throw here, the app crash
override fun onDataChange(snapshot: DataSnapshot) {
snapshot.getValue(Data::class.java) ?: throw RuntimeException("Error while reading data")
}
override fun onCancelled(error: DatabaseError) {
throw error.toException()
}
}
// if I throw here, exception is handled correctly by CoroutineExceptionHandler
val dbRef = getDataRef(id)
dbRef.addValueEventListener(listener)
awaitClose { dbRef.removeEventListener(listener) }
}
And I am collecting the flow like this:
fun getData(id: String) = viewModelScope.launch(errorHandler) {
db.getData(id).collect {
// do something
}
}
Where errorHandler is:
val errorHandler: CoroutineExceptionHandler
get() = CoroutineExceptionHandler { _, throwable ->
// do something with error
}
If I throw an exception from withing the onDataChange
or the onCancelled
methods the app crash.
Is it possible to let the callbackFlow scope handle the exception? Am I throwing it in a wrong way? Should I call trySend
with a proper sealed class instead of throwing an exception (and maybe decide in the receiver what to do with it)?
What's the best way to handle such a situation? Thanks in advance.
CodePudding user response:
Callbacks are executed in the context of the API/database framework, so if we throw inside the callback, we actually crash that API/database component, not the flow.
To send the error through the flow you can use a result object as suggested by @alex-mamo . However, if you prefer to send the exception directly to fail on the collect side, you can just cancel the producer scope:
override fun onCancelled(error: DatabaseError) {
[email protected]("error message", error)
}
This is similar to this:
flow {
delay(1000)
throw Exception("error")
}
Also, CoroutineExceptionHandler
is not meant to replace how we handle exceptions in Kotlin. It is a last resort handler for unhandled exceptions, so we can e.g. log them in the way specific to our application. In your case it seems better to use a regular try ... catch
.
CodePudding user response:
It's not mandatory to use coroutines with the Firebase API, but it certainly makes our development easier. Maybe there are also other solutions out there, but this is how I would do:
@ExperimentalCoroutinesApi
fun getDataFromFirebase(id: String) = callbackFlow {
val listener = object : ValueEventListener {
override fun onDataChange(snapshot: DataSnapshot) {
trySend(Result.Success(snapshot.getValue(Data::class.java)))
}
override fun onCancelled(error: DatabaseError) {
trySend(Result.Error(error))
}
}
val dbRef = getDataRef(id)
dbRef.addValueEventListener(listener)
awaitClose {
dbRef.removeEventListener(listener) }
}
In the ViewModel class I would use:
@ExperimentalCoroutinesApi
fun getData(id: String) = liveData(Dispatchers.IO) {
repository.getDataFromFirebase(id).collect { response ->
emit(response)
}
}
And inside the activity class I would use something like this:
@ExperimentalCoroutinesApi
private fun getData() {
viewModel.getData().observe(this) { response ->
when(response) {
is Result.Success -> print("Success")
is Result.Error -> print("Error")
}
}
}